# Copyright (C) 2010-2024 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

import logging
import collections

from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict
from pyramid.renderers import render
from pyramid.response import Response

from rhodecode.apps._base import RepoAppView
from rhodecode.apps.file_store import utils as store_utils
from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException

from rhodecode.lib import diffs, codeblocks, channelstream
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired
from rhodecode.lib import ext_json
from collections import OrderedDict
from rhodecode.lib.diffs import (
    cache_diff,
    load_cached_diff,
    diff_cache_exist,
    get_diff_context,
    get_diff_whitespace_flag,
)
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
import rhodecode.lib.helpers as h
from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import RepositoryError, CommitDoesNotExistError
from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, ChangesetCommentHistory
from rhodecode.model.changeset_status import ChangesetStatusModel
from rhodecode.model.comment import CommentsModel
from rhodecode.model.meta import Session
from rhodecode.model.settings import VcsSettingsModel

log = logging.getLogger(__name__)


def _update_with_GET(params, request):
    for k in ["diff1", "diff2", "diff"]:
        params[k] += request.GET.getall(k)


class RepoCommitsView(RepoAppView):
    def load_default_context(self):
        c = self._get_local_tmpl_context(include_app_defaults=True)
        c.rhodecode_repo = self.rhodecode_vcs_repo

        return c

    def _is_diff_cache_enabled(self, target_repo):
        caching_enabled = self._get_general_setting(target_repo, "rhodecode_diff_cache")
        log.debug("Diff caching enabled: %s", caching_enabled)
        return caching_enabled

    def _commit(self, commit_id_range, method):
        _ = self.request.translate
        c = self.load_default_context()
        c.fulldiff = self.request.GET.get("fulldiff")
        redirect_to_combined = str2bool(self.request.GET.get("redirect_combined"))

        # fetch global flags of ignore ws or context lines
        diff_context = get_diff_context(self.request)
        hide_whitespace_changes = get_diff_whitespace_flag(self.request)

        # diff_limit will cut off the whole diff if the limit is applied
        # otherwise it will just hide the big files from the front-end
        diff_limit = c.visual.cut_off_limit_diff
        file_limit = c.visual.cut_off_limit_file

        # get ranges of commit ids if preset
        commit_range = commit_id_range.split("...")[:2]

        try:
            pre_load = ["author", "branches", "date", "message", "parents"]
            if self.rhodecode_vcs_repo.alias == "hg":
                pre_load += ["hidden", "obsolete", "phase"]

            if len(commit_range) == 2:
                commits = self.rhodecode_vcs_repo.get_commits(
                    start_id=commit_range[0], end_id=commit_range[1], pre_load=pre_load, translate_tags=False
                )
                commits = list(commits)
            else:
                commits = [self.rhodecode_vcs_repo.get_commit(commit_id=commit_id_range, pre_load=pre_load)]

            c.commit_ranges = commits
            if not c.commit_ranges:
                raise RepositoryError("The commit range returned an empty result")
        except CommitDoesNotExistError as e:
            msg = _("No such commit exists. Org exception: `{}`").format(safe_str(e))
            h.flash(msg, category="error")
            raise HTTPNotFound()
        except Exception:
            log.exception("General failure")
            raise HTTPNotFound()
        single_commit = len(c.commit_ranges) == 1

        if redirect_to_combined and not single_commit:
            source_ref = getattr(
                c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), "raw_id"
            )
            target_ref = c.commit_ranges[-1].raw_id
            next_url = h.route_path(
                "repo_compare",
                repo_name=c.repo_name,
                source_ref_type="rev",
                source_ref=source_ref,
                target_ref_type="rev",
                target_ref=target_ref,
            )
            raise HTTPFound(next_url)

        c.changes = OrderedDict()
        c.lines_added = 0
        c.lines_deleted = 0

        # auto collapse if we have more than limit
        collapse_limit = diffs.DiffProcessor._collapse_commits_over
        c.collapse_all_commits = len(c.commit_ranges) > collapse_limit

        c.commit_statuses = ChangesetStatus.STATUSES
        c.inline_comments = []
        c.files = []

        c.comments = []
        c.unresolved_comments = []
        c.resolved_comments = []

        # Single commit
        if single_commit:
            commit = c.commit_ranges[0]
            c.comments = CommentsModel().get_comments(self.db_repo.repo_id, revision=commit.raw_id)

            # comments from PR
            statuses = ChangesetStatusModel().get_statuses(self.db_repo.repo_id, commit.raw_id, with_revisions=True)

            prs = set()
            reviewers = list()
            reviewers_duplicates = set()  # to not have duplicates from multiple votes
            for c_status in statuses:
                # extract associated pull-requests from votes
                if c_status.pull_request:
                    prs.add(c_status.pull_request)

                # extract reviewers
                _user_id = c_status.author.user_id
                if _user_id not in reviewers_duplicates:
                    reviewers.append(
                        StrictAttributeDict(
                            {
                                "user": c_status.author,
                                # fake attributed for commit, page that we don't have
                                # but we share the display with PR page
                                "mandatory": False,
                                "reasons": [],
                                "rule_user_group_data": lambda: None,
                            }
                        )
                    )
                    reviewers_duplicates.add(_user_id)

            c.reviewers_count = len(reviewers)
            c.observers_count = 0

            # from associated statuses, check the pull requests, and
            # show comments from them
            for pr in prs:
                c.comments.extend(pr.comments)

            c.unresolved_comments = CommentsModel().get_commit_unresolved_todos(commit.raw_id)
            c.resolved_comments = CommentsModel().get_commit_resolved_todos(commit.raw_id)

            c.inline_comments_flat = CommentsModel().get_commit_inline_comments(commit.raw_id)

            review_statuses = ChangesetStatusModel().aggregate_votes_by_user(statuses, reviewers)

            c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED

            c.commit_set_reviewers_data_json = collections.OrderedDict({"reviewers": []})

            for review_obj, member, reasons, mandatory, status in review_statuses:
                member_reviewer = h.reviewer_as_json(
                    member, reasons=reasons, mandatory=mandatory, role=None, user_group=None
                )

                current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
                member_reviewer["review_status"] = current_review_status
                member_reviewer["review_status_label"] = h.commit_status_lbl(current_review_status)
                member_reviewer["allowed_to_update"] = False
                c.commit_set_reviewers_data_json["reviewers"].append(member_reviewer)

            c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)

            # NOTE(marcink): this uses the same voting logic as in pull-requests
            c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
            c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)

        diff = None
        # Iterate over ranges (default commit view is always one commit)
        for commit in c.commit_ranges:
            c.changes[commit.raw_id] = []

            commit2 = commit
            commit1 = commit.first_parent

            if method == "show":
                inline_comments = CommentsModel().get_inline_comments(self.db_repo.repo_id, revision=commit.raw_id)
                c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(inline_comments))
                c.inline_comments = inline_comments

                cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(self.db_repo)
                cache_file_path = diff_cache_exist(
                    cache_path, "diff", commit.raw_id, hide_whitespace_changes, diff_context, c.fulldiff
                )

                caching_enabled = self._is_diff_cache_enabled(self.db_repo)
                force_recache = str2bool(self.request.GET.get("force_recache"))

                cached_diff = None
                if caching_enabled:
                    cached_diff = load_cached_diff(cache_file_path)

                has_proper_diff_cache = cached_diff and cached_diff.get("diff")
                if not force_recache and has_proper_diff_cache:
                    diffset = cached_diff["diff"]
                else:
                    vcs_diff = self.rhodecode_vcs_repo.get_diff(
                        commit1, commit2, ignore_whitespace=hide_whitespace_changes, context=diff_context
                    )

                    diff_processor = diffs.DiffProcessor(
                        vcs_diff,
                        diff_format="newdiff",
                        diff_limit=diff_limit,
                        file_limit=file_limit,
                        show_full_diff=c.fulldiff,
                    )

                    _parsed = diff_processor.prepare()

                    diffset = codeblocks.DiffSet(
                        repo_name=self.db_repo_name,
                        source_node_getter=codeblocks.diffset_node_getter(commit1),
                        target_node_getter=codeblocks.diffset_node_getter(commit2),
                    )

                    diffset = self.path_filter.render_patchset_filtered(
                        diffset, _parsed, commit1.raw_id, commit2.raw_id
                    )

                    # save cached diff
                    if caching_enabled:
                        cache_diff(cache_file_path, diffset, None)

                c.limited_diff = diffset.limited_diff
                c.changes[commit.raw_id] = diffset
            else:
                # TODO(marcink): no cache usage here...
                _diff = self.rhodecode_vcs_repo.get_diff(
                    commit1, commit2, ignore_whitespace=hide_whitespace_changes, context=diff_context
                )
                diff_processor = diffs.DiffProcessor(
                    _diff,
                    diff_format="newdiff",
                    diff_limit=diff_limit,
                    file_limit=file_limit,
                    show_full_diff=c.fulldiff,
                )
                # downloads/raw we only need RAW diff nothing else
                diff = self.path_filter.get_raw_patch(diff_processor)
                c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]

        # sort comments by how they were generated
        c.comments = sorted(c.comments, key=lambda x: x.comment_id)
        c.at_version_num = None

        if len(c.commit_ranges) == 1:
            c.commit = c.commit_ranges[0]
            c.parent_tmpl = "".join("# Parent  %s\n" % x.raw_id for x in c.commit.parents)

        if method == "download":
            response = Response(diff)
            response.content_type = "text/plain"
            response.content_disposition = "attachment; filename=%s.diff" % commit_id_range[:12]
            return response
        elif method == "patch":
            c.diff = safe_str(diff)
            patch = render(
                "rhodecode:templates/changeset/patch_changeset.mako", self._get_template_context(c), self.request
            )
            response = Response(patch)
            response.content_type = "text/plain"
            return response
        elif method == "raw":
            response = Response(diff)
            response.content_type = "text/plain"
            return response
        elif method == "show":
            if len(c.commit_ranges) == 1:
                html = render(
                    "rhodecode:templates/changeset/changeset.mako", self._get_template_context(c), self.request
                )
                return Response(html)
            else:
                c.ancestor = None
                c.target_repo = self.db_repo
                html = render(
                    "rhodecode:templates/changeset/changeset_range.mako", self._get_template_context(c), self.request
                )
                return Response(html)

        raise HTTPBadRequest()

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_show(self):
        commit_id = self.request.matchdict["commit_id"]
        return self._commit(commit_id, method="show")

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_raw(self):
        commit_id = self.request.matchdict["commit_id"]
        return self._commit(commit_id, method="raw")

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_patch(self):
        commit_id = self.request.matchdict["commit_id"]
        return self._commit(commit_id, method="patch")

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_download(self):
        commit_id = self.request.matchdict["commit_id"]
        return self._commit(commit_id, method="download")

    def _commit_comments_create(self, commit_id, comments):
        _ = self.request.translate
        data = {}
        if not comments:
            return

        commit = self.db_repo.get_commit(commit_id)

        all_drafts = len([x for x in comments if str2bool(x["is_draft"])]) == len(comments)
        for entry in comments:
            c = self.load_default_context()
            comment_type = entry["comment_type"]
            text = entry["text"]
            status = entry["status"]
            is_draft = str2bool(entry["is_draft"])
            resolves_comment_id = entry["resolves_comment_id"]
            f_path = entry["f_path"]
            line_no = entry["line"]
            target_elem_id = f"file-{h.safeid(h.safe_str(f_path))}"

            if status:
                text = text or (
                    _("Status change %(transition_icon)s %(status)s")
                    % {"transition_icon": ">", "status": ChangesetStatus.get_status_lbl(status)}
                )

            comment = CommentsModel().create(
                text=text,
                repo=self.db_repo.repo_id,
                user=self._rhodecode_db_user.user_id,
                commit_id=commit_id,
                f_path=f_path,
                line_no=line_no,
                status_change=(ChangesetStatus.get_status_lbl(status) if status else None),
                status_change_type=status,
                comment_type=comment_type,
                is_draft=is_draft,
                resolves_comment_id=resolves_comment_id,
                auth_user=self._rhodecode_user,
                send_email=not is_draft,  # skip notification for draft comments
            )
            is_inline = comment.is_inline

            # get status if set !
            if status:
                # `dont_allow_on_closed_pull_request = True` means
                # if latest status was from pull request and it's closed
                # disallow changing status !

                try:
                    ChangesetStatusModel().set_status(
                        self.db_repo.repo_id,
                        status,
                        self._rhodecode_db_user.user_id,
                        comment,
                        revision=commit_id,
                        dont_allow_on_closed_pull_request=True,
                    )
                except StatusChangeOnClosedPullRequestError:
                    msg = _("Changing the status of a commit associated with a closed pull request is not allowed")
                    log.exception(msg)
                    h.flash(msg, category="warning")
                    raise HTTPFound(h.route_path("repo_commit", repo_name=self.db_repo_name, commit_id=commit_id))

            Session().flush()
            # this is somehow required to get access to some relationship
            # loaded on comment
            Session().refresh(comment)

            # skip notifications for drafts
            if not is_draft:
                CommentsModel().trigger_commit_comment_hook(
                    self.db_repo, self._rhodecode_user, "create", data={"comment": comment, "commit": commit}
                )

            comment_id = comment.comment_id
            data[comment_id] = {"target_id": target_elem_id}
            Session().flush()

            c.co = comment
            c.at_version_num = 0
            c.is_new = True
            rendered_comment = render(
                "rhodecode:templates/changeset/changeset_comment_block.mako",
                self._get_template_context(c),
                self.request,
            )

            data[comment_id].update(comment.get_dict())
            data[comment_id].update({"rendered_text": rendered_comment})

        # finalize, commit and redirect
        Session().commit()

        # skip channelstream for draft comments
        if not all_drafts:
            comment_broadcast_channel = channelstream.comment_channel(self.db_repo_name, commit_obj=commit)

            comment_data = data
            posted_comment_type = "inline" if is_inline else "general"
            if len(data) == 1:
                msg = _("posted {} new {} comment").format(len(data), posted_comment_type)
            else:
                msg = _("posted {} new {} comments").format(len(data), posted_comment_type)

            channelstream.comment_channelstream_push(
                self.request, comment_broadcast_channel, self._rhodecode_user, msg, comment_data=comment_data
            )

        return data

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_create(self):
        _ = self.request.translate
        commit_id = self.request.matchdict["commit_id"]

        multi_commit_ids = []
        for _commit_id in self.request.POST.get("commit_ids", "").split(","):
            if _commit_id not in ["", None, EmptyCommit.raw_id]:
                if _commit_id not in multi_commit_ids:
                    multi_commit_ids.append(_commit_id)

        commit_ids = multi_commit_ids or [commit_id]

        data = []
        # Multiple comments for each passed commit id
        for current_id in filter(None, commit_ids):
            comment_data = {
                "comment_type": self.request.POST.get("comment_type"),
                "text": self.request.POST.get("text"),
                "status": self.request.POST.get("changeset_status", None),
                "is_draft": self.request.POST.get("draft"),
                "resolves_comment_id": self.request.POST.get("resolves_comment_id", None),
                "close_pull_request": self.request.POST.get("close_pull_request"),
                "f_path": self.request.POST.get("f_path"),
                "line": self.request.POST.get("line"),
            }
            comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
            data.append(comment)

        return data if len(data) > 1 else data[0]

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_preview(self):
        # Technically a CSRF token is not needed as no state changes with this
        # call. However, as this is a POST is better to have it, so automated
        # tools don't flag it as potential CSRF.
        # Post is required because the payload could be bigger than the maximum
        # allowed by GET.

        text = self.request.POST.get("text")
        renderer = self.request.POST.get("renderer") or "rst"
        if text:
            return h.render(text, renderer=renderer, mentions=True, repo_name=self.db_repo_name)
        return ""

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_history_view(self):
        c = self.load_default_context()
        comment_id = self.request.matchdict["comment_id"]
        comment_history_id = self.request.matchdict["comment_history_id"]

        comment = ChangesetComment.get_or_404(comment_id)
        comment_owner = comment.author.user_id == self._rhodecode_db_user.user_id
        if comment.draft and not comment_owner:
            # if we see draft comments history, we only allow this for owner
            raise HTTPNotFound()

        comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
        is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id

        if is_repo_comment:
            c.comment_history = comment_history

            rendered_comment = render(
                "rhodecode:templates/changeset/comment_history.mako", self._get_template_context(c), self.request
            )
            return rendered_comment
        else:
            log.warning(
                "No permissions for user %s to show comment_history_id: %s", self._rhodecode_db_user, comment_history_id
            )
            raise HTTPNotFound()

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_attachment_upload(self):
        c = self.load_default_context()
        upload_key = "attachment"

        file_obj = self.request.POST.get(upload_key)

        if file_obj is None:
            self.request.response.status = 400
            return {"store_fid": None, "access_path": None, "error": f"{upload_key} data field is missing"}

        if not hasattr(file_obj, "filename"):
            self.request.response.status = 400
            return {"store_fid": None, "access_path": None, "error": "filename cannot be read from the data field"}

        filename = file_obj.filename
        file_display_name = filename

        metadata = {
            "user_uploaded": {
                "username": self._rhodecode_user.username,
                "user_id": self._rhodecode_user.user_id,
                "ip": self._rhodecode_user.ip_addr,
            }
        }

        # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
        allowed_extensions = [
            "gif",
            ".jpeg",
            ".jpg",
            ".png",
            ".docx",
            ".gz",
            ".log",
            ".pdf",
            ".pptx",
            ".txt",
            ".xlsx",
            ".zip",
        ]
        max_file_size = 10 * 1024 * 1024  # 10MB

        try:
            f_store = store_utils.get_filestore_backend(self.request.registry.settings)
            store_uid, metadata = f_store.store(
                filename, file_obj.file, metadata=metadata, extensions=allowed_extensions, max_filesize=max_file_size
            )
        except FileNotAllowedException:
            self.request.response.status = 400
            permitted_extensions = ", ".join(allowed_extensions)
            error_msg = (
                f"File `{filename}` is not allowed. Only following extensions are permitted: {permitted_extensions}"
            )

            return {"store_fid": None, "access_path": None, "error": error_msg}
        except FileOverSizeException:
            self.request.response.status = 400
            limit_mb = h.format_byte_size_binary(max_file_size)
            error_msg = f"File {filename} is exceeding allowed limit of {limit_mb}."
            return {"store_fid": None, "access_path": None, "error": error_msg}

        try:
            entry = FileStore.create(
                file_uid=store_uid,
                filename=metadata["filename"],
                file_hash=metadata["sha256"],
                file_size=metadata["size"],
                file_display_name=file_display_name,
                file_description=f"comment attachment `{safe_str(filename)}`",
                hidden=True,
                check_acl=True,
                user_id=self._rhodecode_user.user_id,
                scope_repo_id=self.db_repo.repo_id,
            )
            Session().add(entry)
            Session().commit()
            log.debug("Stored upload in DB as %s", entry)
        except Exception:
            log.exception("Failed to store file %s", filename)
            self.request.response.status = 400
            return {"store_fid": None, "access_path": None, "error": f"File {filename} failed to store in DB."}

        Session().commit()

        data = {
            "store_fid": store_uid,
            "access_path": h.route_path("download_file", fid=store_uid),
            "fqn_access_path": h.route_url("download_file", fid=store_uid),
            # for EE those are replaced by FQN links on repo-only like
            "repo_access_path": h.route_url("download_file", fid=store_uid),
            "repo_fqn_access_path": h.route_url("download_file", fid=store_uid),
        }
        # this data is a part of CE/EE additional code
        if c.rhodecode_edition_id == "EE":
            data.update(
                {
                    "repo_access_path": h.route_path("repo_artifacts_get", repo_name=self.db_repo_name, uid=store_uid),
                    "repo_fqn_access_path": h.route_url(
                        "repo_artifacts_get", repo_name=self.db_repo_name, uid=store_uid
                    ),
                }
            )

        return data

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_delete(self):
        commit_id = self.request.matchdict["commit_id"]
        comment_id = self.request.matchdict["comment_id"]

        comment = ChangesetComment.get_or_404(comment_id)
        if not comment:
            log.debug("Comment with id:%s not found, skipping", comment_id)
            # comment already deleted in another call probably
            return True

        if comment.immutable:
            # don't allow deleting comments that are immutable
            raise HTTPForbidden()

        is_repo_admin = h.HasRepoPermissionAny("repository.admin")(self.db_repo_name)
        super_admin = h.HasPermissionAny("hg.admin")()
        comment_owner = comment.author.user_id == self._rhodecode_db_user.user_id
        is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
        comment_repo_admin = is_repo_admin and is_repo_comment

        if comment.draft and not comment_owner:
            # We never allow to delete draft comments for other than owners
            raise HTTPNotFound()

        if super_admin or comment_owner or comment_repo_admin:
            CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
            Session().commit()
            return True
        else:
            log.warning("No permissions for user %s to delete comment_id: %s", self._rhodecode_db_user, comment_id)
            raise HTTPNotFound()

    @LoginRequired()
    @NotAnonymous()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    @CSRFRequired()
    def repo_commit_comment_edit(self):
        self.load_default_context()

        commit_id = self.request.matchdict["commit_id"]
        comment_id = self.request.matchdict["comment_id"]
        comment = ChangesetComment.get_or_404(comment_id)

        if comment.immutable:
            # don't allow deleting comments that are immutable
            raise HTTPForbidden()

        is_repo_admin = h.HasRepoPermissionAny("repository.admin")(self.db_repo_name)
        super_admin = h.HasPermissionAny("hg.admin")()
        comment_owner = comment.author.user_id == self._rhodecode_db_user.user_id
        is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
        comment_repo_admin = is_repo_admin and is_repo_comment

        if super_admin or comment_owner or comment_repo_admin:
            text = self.request.POST.get("text")
            version = self.request.POST.get("version")
            if text == comment.text:
                log.warning(
                    "Comment(repo): Trying to create new version with the same comment body {}".format(
                        comment_id,
                    )
                )
                raise HTTPNotFound()

            if version.isdigit():
                version = int(version)
            else:
                log.warning(
                    "Comment(repo): Wrong version type {} {} for comment {}".format(
                        version,
                        type(version),
                        comment_id,
                    )
                )
                raise HTTPNotFound()

            try:
                comment_history = CommentsModel().edit(
                    comment_id=comment_id,
                    text=text,
                    auth_user=self._rhodecode_user,
                    version=version,
                )
            except CommentVersionMismatch:
                raise HTTPConflict()

            if not comment_history:
                raise HTTPNotFound()

            if not comment.draft:
                commit = self.db_repo.get_commit(commit_id)
                CommentsModel().trigger_commit_comment_hook(
                    self.db_repo, self._rhodecode_user, "edit", data={"comment": comment, "commit": commit}
                )

            Session().commit()
            return {
                "comment_history_id": comment_history.comment_history_id,
                "comment_id": comment.comment_id,
                "comment_version": comment_history.version,
                "comment_author_username": comment_history.author.username,
                "comment_author_gravatar": h.gravatar_url(comment_history.author.email, 16, request=self.request),
                "comment_created_on": h.age_component(comment_history.created_on, time_is_local=True),
            }
        else:
            log.warning("No permissions for user %s to edit comment_id: %s", self._rhodecode_db_user, comment_id)
            raise HTTPNotFound()

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_data(self):
        commit_id = self.request.matchdict["commit_id"]
        self.load_default_context()

        try:
            return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
        except CommitDoesNotExistError as e:
            return EmptyCommit(message=str(e))

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_children(self):
        commit_id = self.request.matchdict["commit_id"]
        self.load_default_context()

        try:
            commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
            children = commit.children
        except CommitDoesNotExistError:
            children = []

        result = {"results": children}
        return result

    @LoginRequired()
    @HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
    def repo_commit_parents(self):
        commit_id = self.request.matchdict["commit_id"]
        self.load_default_context()

        try:
            commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
            parents = commit.parents
        except CommitDoesNotExistError:
            parents = []
        result = {"results": parents}
        return result
