pagure_hook.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import absolute_import, unicode_literals
  8. import logging
  9. import pygit2
  10. import sqlalchemy as sa
  11. import wtforms
  12. try:
  13. from flask_wtf import FlaskForm
  14. except ImportError:
  15. from flask_wtf import Form as FlaskForm
  16. from sqlalchemy.exc import SQLAlchemyError
  17. from sqlalchemy.orm import backref, relation
  18. import pagure.config
  19. import pagure.lib.git
  20. import pagure.lib.query
  21. from pagure.hooks import BaseHook, BaseRunner
  22. from pagure.lib.model import BASE, Project
  23. _log = logging.getLogger(__name__)
  24. pagure_config = pagure.config.reload_config()
  25. class PagureTable(BASE):
  26. """Stores information about the pagure hook deployed on a project.
  27. Table -- hook_pagure
  28. """
  29. __tablename__ = "hook_pagure"
  30. id = sa.Column(sa.Integer, primary_key=True)
  31. project_id = sa.Column(
  32. sa.Integer,
  33. sa.ForeignKey("projects.id", onupdate="CASCADE", ondelete="CASCADE"),
  34. nullable=False,
  35. unique=True,
  36. index=True,
  37. )
  38. active = sa.Column(sa.Boolean, nullable=False, default=False)
  39. project = relation(
  40. "Project",
  41. remote_side=[Project.id],
  42. backref=backref(
  43. "pagure_hook",
  44. cascade="delete, delete-orphan",
  45. single_parent=True,
  46. uselist=False,
  47. ),
  48. )
  49. def generate_revision_change_log(
  50. session, project, username, repodir, new_commits_list
  51. ):
  52. print("Detailed log of new commits:\n\n")
  53. commitid = None
  54. for line in pagure.lib.git.read_git_lines(
  55. ["log", "--no-walk"] + new_commits_list + ["--"], repodir
  56. ):
  57. if line.startswith("commit"):
  58. commitid = line.split("commit ")[-1]
  59. line = line.strip()
  60. print("*", line)
  61. for issue_or_pr in pagure.lib.link.get_relation(
  62. session,
  63. project.name,
  64. project.username if project.is_fork else None,
  65. project.namespace,
  66. line,
  67. "fixes",
  68. include_prs=True,
  69. ):
  70. if pagure_config.get("HOOK_DEBUG", False):
  71. print(commitid, relation)
  72. fixes_relation(
  73. session,
  74. username,
  75. commitid,
  76. issue_or_pr,
  77. pagure_config.get("APP_URL"),
  78. )
  79. for issue in pagure.lib.link.get_relation(
  80. session,
  81. project.name,
  82. project.username if project.is_fork else None,
  83. project.namespace,
  84. line,
  85. "relates",
  86. ):
  87. if pagure_config.get("HOOK_DEBUG", False):
  88. print(commitid, issue)
  89. relates_commit(
  90. session,
  91. username,
  92. commitid,
  93. issue,
  94. pagure_config.get("APP_URL"),
  95. )
  96. def relates_commit(session, username, commitid, issue, app_url=None):
  97. """Add a comment to an issue that this commit relates to it."""
  98. url = "../%s" % commitid[:8]
  99. if app_url:
  100. if app_url.endswith("/"):
  101. app_url = app_url[:-1]
  102. project = issue.project.fullname
  103. if issue.project.is_fork:
  104. project = "fork/%s" % project
  105. url = "%s/%s/c/%s" % (app_url, project, commitid[:8])
  106. comment = """ Commit [%s](%s) relates to this ticket""" % (
  107. commitid[:8],
  108. url,
  109. )
  110. try:
  111. pagure.lib.query.add_issue_comment(
  112. session, issue=issue, comment=comment, user=username
  113. )
  114. session.commit()
  115. except pagure.exceptions.PagureException as err:
  116. print(err)
  117. except SQLAlchemyError as err: # pragma: no cover
  118. session.rollback()
  119. _log.exception(err)
  120. def fixes_relation(session, username, commitid, relation, app_url=None):
  121. """Add a comment to an issue or PR that this commit fixes it and update
  122. the status if the commit is in the master branch."""
  123. url = "../c/%s" % commitid[:8]
  124. if app_url:
  125. if app_url.endswith("/"):
  126. app_url = app_url[:-1]
  127. project = relation.project.fullname
  128. if relation.project.is_fork:
  129. project = "fork/%s" % project
  130. url = "%s/%s/c/%s" % (app_url, project, commitid[:8])
  131. comment = """ Commit [%s](%s) fixes this %s""" % (
  132. commitid[:8],
  133. url,
  134. relation.isa,
  135. )
  136. try:
  137. if relation.isa == "issue":
  138. pagure.lib.query.add_issue_comment(
  139. session, issue=relation, comment=comment, user=username
  140. )
  141. elif relation.isa == "pull-request":
  142. pagure.lib.query.add_pull_request_comment(
  143. session,
  144. request=relation,
  145. commit=None,
  146. tree_id=None,
  147. filename=None,
  148. row=None,
  149. comment=comment,
  150. user=username,
  151. )
  152. session.commit()
  153. except pagure.exceptions.PagureException as err:
  154. print(err)
  155. except SQLAlchemyError as err: # pragma: no cover
  156. session.rollback()
  157. _log.exception(err)
  158. try:
  159. if relation.isa == "issue":
  160. pagure.lib.query.edit_issue(
  161. session,
  162. relation,
  163. user=username,
  164. status="Closed",
  165. close_status="Fixed",
  166. )
  167. elif relation.isa == "pull-request":
  168. pagure.lib.query.close_pull_request(
  169. session, relation, user=username, merged=True
  170. )
  171. session.commit()
  172. except pagure.exceptions.PagureException as err:
  173. print(err)
  174. except SQLAlchemyError as err: # pragma: no cover
  175. session.rollback()
  176. print("ERROR", err)
  177. _log.exception(err)
  178. class PagureRunner(BaseRunner):
  179. """Runner for the pagure's specific git hook."""
  180. @staticmethod
  181. def post_receive(
  182. session, username, project, repotype, repodir, changes, pull_request
  183. ):
  184. """Run the default post-receive hook.
  185. For args, see BaseRunner.runhook.
  186. """
  187. if repotype != "main":
  188. print("The pagure hook only runs on the main git repo.")
  189. return
  190. for refname in changes:
  191. (oldrev, newrev) = changes[refname]
  192. # Retrieve the default branch
  193. repo_obj = pygit2.Repository(repodir)
  194. default_branch = None
  195. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  196. default_branch = repo_obj.head.shorthand
  197. # Skip all branch but the default one
  198. refname = refname.replace("refs/heads/", "")
  199. if refname != default_branch:
  200. continue
  201. if set(newrev) == set(["0"]):
  202. print(
  203. "Deleting a reference/branch, so we won't run the "
  204. "pagure hook"
  205. )
  206. return
  207. generate_revision_change_log(
  208. session,
  209. project,
  210. username,
  211. repodir,
  212. pagure.lib.git.get_revs_between(
  213. oldrev, newrev, repodir, refname
  214. ),
  215. )
  216. session.close()
  217. class PagureForm(FlaskForm):
  218. """Form to configure the pagure hook."""
  219. active = wtforms.BooleanField("Active", [wtforms.validators.Optional()])
  220. DESCRIPTION = """
  221. Pagure specific hook to add a comment to issues or pull requests if the pushed
  222. commits fix them
  223. or relate to them. This is determined based on the commit message.
  224. To reference an issue/PR you need to use one of recognized keywords followed by
  225. a reference to the issue or PR, separated by whitespace and and optional colon.
  226. Such references can be either:
  227. * The issue/PR number preceded by the `#` symbol
  228. * The full URL of the issue or PR
  229. If using the full URL, it is possible to reference issues in other projects.
  230. The recognized keywords are:
  231. * fix/fixed/fixes
  232. * relate/related/relates
  233. * merge/merges/merged
  234. * close/closes/closed
  235. Examples:
  236. * Fixes #21
  237. * related: https://pagure.io/myproject/issue/32
  238. * this commit merges #74
  239. * Merged: https://pagure.io/myproject/pull-request/74
  240. Capitalization does not matter; neither does the colon between keyword and
  241. number.
  242. """
  243. class PagureHook(BaseHook):
  244. """Pagure hook."""
  245. name = "Pagure"
  246. description = DESCRIPTION
  247. form = PagureForm
  248. db_object = PagureTable
  249. backref = "pagure_hook"
  250. form_fields = ["active"]
  251. runner = PagureRunner