pagure_hook.py 8.4 KB

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