fork.py 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2017 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. # pylint: disable=too-many-return-statements
  8. # pylint: disable=too-many-branches
  9. # pylint: disable=too-many-arguments
  10. # pylint: disable=too-many-locals
  11. # pylint: disable=too-many-statements
  12. # pylint: disable=too-many-lines
  13. from __future__ import absolute_import, unicode_literals
  14. import json
  15. import logging
  16. import os
  17. from math import ceil
  18. import flask
  19. import pygit2
  20. from sqlalchemy.exc import SQLAlchemyError
  21. import pagure
  22. import pagure.doc_utils
  23. import pagure.exceptions
  24. import pagure.forms
  25. import pagure.lib.git
  26. import pagure.lib.plugins
  27. import pagure.lib.query
  28. import pagure.lib.tasks
  29. from pagure.config import config as pagure_config
  30. from pagure.ui import UI_NS
  31. from pagure.utils import (
  32. __get_file_in_tree,
  33. get_parent_repo_path,
  34. is_true,
  35. login_required,
  36. )
  37. _log = logging.getLogger(__name__)
  38. def _get_parent_request_repo_path(repo):
  39. """Return the path of the parent git repository corresponding to the
  40. provided Repository object from the DB.
  41. """
  42. if repo.parent:
  43. return repo.parent.repopath("requests")
  44. else:
  45. return repo.repopath("requests")
  46. @UI_NS.route("/<repo>/pull-requests/")
  47. @UI_NS.route("/<repo>/pull-requests")
  48. @UI_NS.route("/<namespace>/<repo>/pull-requests/")
  49. @UI_NS.route("/<namespace>/<repo>/pull-requests")
  50. @UI_NS.route("/fork/<username>/<repo>/pull-requests/")
  51. @UI_NS.route("/fork/<username>/<repo>/pull-requests")
  52. @UI_NS.route("/fork/<username>/<namespace>/<repo>/pull-requests/")
  53. @UI_NS.route("/fork/<username>/<namespace>/<repo>/pull-requests")
  54. def request_pulls(repo, username=None, namespace=None):
  55. """List all Pull-requests associated to a repo"""
  56. status = flask.request.args.get("status", "Open")
  57. tags = flask.request.args.getlist("tags")
  58. tags = [tag.strip() for tag in tags if tag.strip()]
  59. assignee = flask.request.args.get("assignee", None)
  60. search_pattern = flask.request.args.get("search_pattern", None)
  61. author = flask.request.args.get("author", None)
  62. order = flask.request.args.get("order", "desc")
  63. order_key = flask.request.args.get("order_key", "date_created")
  64. repo = flask.g.repo
  65. if not repo.settings.get("pull_requests", True):
  66. flask.abort(404, description="No pull-requests found for this project")
  67. total_open = pagure.lib.query.search_pull_requests(
  68. flask.g.session, project_id=repo.id, status=True, count=True
  69. )
  70. total_merged = pagure.lib.query.search_pull_requests(
  71. flask.g.session, project_id=repo.id, status="Merged", count=True
  72. )
  73. if status.lower() == "merged" or is_true(status, ["false", "0"]):
  74. status_filter = "Merged"
  75. requests = pagure.lib.query.search_pull_requests(
  76. flask.g.session,
  77. project_id=repo.id,
  78. status="Merged",
  79. order=order,
  80. order_key=order_key,
  81. assignee=assignee,
  82. author=author,
  83. tags=tags,
  84. search_pattern=search_pattern,
  85. offset=flask.g.offset,
  86. limit=flask.g.limit,
  87. )
  88. elif is_true(status, ["true", "1", "open"]):
  89. status_filter = "Open"
  90. requests = pagure.lib.query.search_pull_requests(
  91. flask.g.session,
  92. project_id=repo.id,
  93. status="Open",
  94. order=order,
  95. order_key=order_key,
  96. assignee=assignee,
  97. author=author,
  98. tags=tags,
  99. search_pattern=search_pattern,
  100. offset=flask.g.offset,
  101. limit=flask.g.limit,
  102. )
  103. elif status.lower() == "closed":
  104. status_filter = "Closed"
  105. requests = pagure.lib.query.search_pull_requests(
  106. flask.g.session,
  107. project_id=repo.id,
  108. status="Closed",
  109. order=order,
  110. order_key=order_key,
  111. assignee=assignee,
  112. author=author,
  113. tags=tags,
  114. search_pattern=search_pattern,
  115. offset=flask.g.offset,
  116. limit=flask.g.limit,
  117. )
  118. else:
  119. status_filter = None
  120. requests = pagure.lib.query.search_pull_requests(
  121. flask.g.session,
  122. project_id=repo.id,
  123. status=None,
  124. order=order,
  125. order_key=order_key,
  126. assignee=assignee,
  127. author=author,
  128. tags=tags,
  129. search_pattern=search_pattern,
  130. offset=flask.g.offset,
  131. limit=flask.g.limit,
  132. )
  133. open_cnt = pagure.lib.query.search_pull_requests(
  134. flask.g.session,
  135. project_id=repo.id,
  136. status="Open",
  137. assignee=assignee,
  138. author=author,
  139. tags=tags,
  140. search_pattern=search_pattern,
  141. count=True,
  142. )
  143. merged_cnt = pagure.lib.query.search_pull_requests(
  144. flask.g.session,
  145. project_id=repo.id,
  146. status="Merged",
  147. assignee=assignee,
  148. author=author,
  149. tags=tags,
  150. search_pattern=search_pattern,
  151. count=True,
  152. )
  153. closed_cnt = pagure.lib.query.search_pull_requests(
  154. flask.g.session,
  155. project_id=repo.id,
  156. status="Closed",
  157. assignee=assignee,
  158. author=author,
  159. tags=tags,
  160. search_pattern=search_pattern,
  161. count=True,
  162. )
  163. repo_obj = flask.g.repo_obj
  164. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  165. head = repo_obj.head.shorthand
  166. else:
  167. head = "master"
  168. total_page = 1
  169. if len(requests):
  170. if status_filter == "Closed":
  171. total_requests = closed_cnt
  172. elif status_filter == "Merged":
  173. total_requests = merged_cnt
  174. elif status_filter == "Open":
  175. total_requests = open_cnt
  176. else:
  177. total_requests = closed_cnt + merged_cnt + open_cnt
  178. total_page = int(ceil(total_requests / float(flask.g.limit)))
  179. tag_list = pagure.lib.query.get_tags_of_project(flask.g.session, repo)
  180. return flask.render_template(
  181. "requests.html",
  182. select="requests",
  183. repo=repo,
  184. username=username,
  185. tag_list=tag_list,
  186. tags=tags,
  187. requests=requests,
  188. open_cnt=open_cnt,
  189. merged_cnt=merged_cnt,
  190. closed_cnt=closed_cnt,
  191. order=order,
  192. order_key=order_key,
  193. status=status,
  194. status_filter=status_filter,
  195. assignee=assignee,
  196. author=author,
  197. search_pattern=search_pattern,
  198. head=head,
  199. total_page=total_page,
  200. total_open=total_open,
  201. total_merged=total_merged,
  202. )
  203. @UI_NS.route("/<repo>/pull-request/<int:requestid>/")
  204. @UI_NS.route("/<repo>/pull-request/<int:requestid>")
  205. @UI_NS.route("/<namespace>/<repo>/pull-request/<int:requestid>/")
  206. @UI_NS.route("/<namespace>/<repo>/pull-request/<int:requestid>")
  207. @UI_NS.route("/fork/<username>/<repo>/pull-request/<int:requestid>/")
  208. @UI_NS.route("/fork/<username>/<repo>/pull-request/<int:requestid>")
  209. @UI_NS.route(
  210. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/"
  211. )
  212. @UI_NS.route(
  213. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>"
  214. )
  215. def request_pull(repo, requestid, username=None, namespace=None):
  216. """View a pull request with the changes from the fork into the project."""
  217. repo = flask.g.repo
  218. _log.info("Viewing pull Request #%s repo: %s", requestid, repo.fullname)
  219. if not repo.settings.get("pull_requests", True):
  220. flask.abort(404, description="No pull-requests found for this project")
  221. request = pagure.lib.query.search_pull_requests(
  222. flask.g.session, project_id=repo.id, requestid=requestid
  223. )
  224. if not request:
  225. flask.abort(404, description="Pull-request not found")
  226. if request.remote:
  227. repopath = pagure.utils.get_remote_repo_path(
  228. request.remote_git, request.branch_from
  229. )
  230. parentpath = pagure.utils.get_repo_path(request.project)
  231. else:
  232. repo_from = request.project_from
  233. parentpath = pagure.utils.get_repo_path(request.project)
  234. repopath = parentpath
  235. if repo_from:
  236. repopath = pagure.utils.get_repo_path(repo_from)
  237. repo_obj = pygit2.Repository(repopath)
  238. orig_repo = pygit2.Repository(parentpath)
  239. diff_commits = []
  240. diff = None
  241. # Closed pull-request
  242. if request.status != "Open":
  243. commitid = request.commit_stop
  244. try:
  245. for commit in repo_obj.walk(commitid, pygit2.GIT_SORT_NONE):
  246. diff_commits.append(commit)
  247. if commit.oid.hex == request.commit_start:
  248. break
  249. except KeyError:
  250. # This happens when repo.walk() cannot find commitid
  251. pass
  252. if diff_commits:
  253. # Ensure the first commit in the PR as a parent, otherwise
  254. # point to it
  255. start = diff_commits[-1].oid.hex
  256. if diff_commits[-1].parents:
  257. start = diff_commits[-1].parents[0].oid.hex
  258. # If the start and the end commits are the same, it means we are,
  259. # dealing with one commit that has no parent, so just diff that
  260. # one commit
  261. if start == diff_commits[0].oid.hex:
  262. diff = diff_commits[0].tree.diff_to_tree(swap=True)
  263. else:
  264. diff = repo_obj.diff(
  265. repo_obj.revparse_single(start),
  266. repo_obj.revparse_single(diff_commits[0].oid.hex),
  267. )
  268. else:
  269. try:
  270. diff_commits, diff = pagure.lib.git.diff_pull_request(
  271. flask.g.session, request, repo_obj, orig_repo
  272. )
  273. except pagure.exceptions.PagureException as err:
  274. flask.flash("%s" % err, "error")
  275. except SQLAlchemyError as err: # pragma: no cover
  276. flask.g.session.rollback()
  277. _log.exception(err)
  278. flask.flash(
  279. "Could not update this pull-request in the database", "error"
  280. )
  281. if diff:
  282. diff.find_similar()
  283. warning_characters = pagure.lib.query.find_warning_characters(
  284. repo_obj, diff_commits
  285. )
  286. form = pagure.forms.MergePRForm()
  287. trigger_ci_pr_form = pagure.forms.TriggerCIPRForm()
  288. # we need to leave out all members of trigger_ci_conf that have
  289. # "meta" set to False or meta["requires_project_hook_attr"] condition
  290. # defined and it's not met
  291. trigger_ci_conf = pagure_config["TRIGGER_CI"]
  292. if not isinstance(trigger_ci_conf, dict):
  293. trigger_ci_conf = {}
  294. trigger_ci = {}
  295. # make sure all the backrefs are set properly on repo
  296. pagure.lib.plugins.get_enabled_plugins(repo)
  297. for comment, meta in trigger_ci_conf.items():
  298. if not meta:
  299. continue
  300. cond = meta.get("requires_project_hook_attr", ())
  301. if cond and not pagure.utils.project_has_hook_attr_value(repo, *cond):
  302. continue
  303. trigger_ci[comment] = meta
  304. committer = False
  305. if request.project_from:
  306. committer = pagure.utils.is_repo_committer(request.project_from)
  307. else:
  308. committer = pagure.utils.is_repo_committer(request.project)
  309. can_rebase_branch = not request.remote_git and committer
  310. can_delete_branch = (
  311. pagure_config.get("ALLOW_DELETE_BRANCH", True) and can_rebase_branch
  312. )
  313. return flask.render_template(
  314. "repo_pull_request.html",
  315. select="requests",
  316. requestid=requestid,
  317. repo=repo,
  318. username=username,
  319. repo_obj=repo_obj,
  320. pull_request=request,
  321. diff_commits=diff_commits,
  322. diff=diff,
  323. mergeform=form,
  324. subscribers=pagure.lib.query.get_watch_list(flask.g.session, request),
  325. tag_list=pagure.lib.query.get_tags_of_project(flask.g.session, repo),
  326. can_rebase_branch=can_rebase_branch,
  327. can_delete_branch=can_delete_branch,
  328. trigger_ci=trigger_ci,
  329. trigger_ci_pr_form=trigger_ci_pr_form,
  330. flag_statuses_labels=json.dumps(pagure_config["FLAG_STATUSES_LABELS"]),
  331. warning_characters=warning_characters,
  332. )
  333. @UI_NS.route("/<repo>/pull-request/<int:requestid>.patch")
  334. @UI_NS.route("/<namespace>/<repo>/pull-request/<int:requestid>.patch")
  335. @UI_NS.route("/fork/<username>/<repo>/pull-request/<int:requestid>.patch")
  336. @UI_NS.route(
  337. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>.patch"
  338. )
  339. def request_pull_patch(repo, requestid, username=None, namespace=None):
  340. """Returns the commits from the specified pull-request as patches."""
  341. return request_pull_to_diff_or_patch(
  342. repo, requestid, username, namespace, diff=False
  343. )
  344. @UI_NS.route("/<repo>/pull-request/<int:requestid>.diff")
  345. @UI_NS.route("/<namespace>/<repo>/pull-request/<int:requestid>.diff")
  346. @UI_NS.route("/fork/<username>/<repo>/pull-request/<int:requestid>.diff")
  347. @UI_NS.route(
  348. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>.diff"
  349. )
  350. def request_pull_diff(repo, requestid, username=None, namespace=None):
  351. """Returns the commits from the specified pull-request as patches."""
  352. return request_pull_to_diff_or_patch(
  353. repo, requestid, username, namespace, diff=True
  354. )
  355. def request_pull_to_diff_or_patch(
  356. repo, requestid, username=None, namespace=None, diff=False
  357. ):
  358. """Returns the commits from the specified pull-request as patches.
  359. :arg repo: the `pagure.lib.model.Project` object of the current pagure
  360. project browsed
  361. :type repo: `pagure.lib.model.Project`
  362. :arg requestid: the identifier of the pull-request to convert to patch
  363. or diff
  364. :type requestid: int
  365. :kwarg username: the username of the user who forked then project when
  366. the project viewed is a fork
  367. :type username: str or None
  368. :kwarg namespace: the namespace of the project if it has one
  369. :type namespace: str or None
  370. :kwarg diff: a boolean whether the data returned is a patch or a diff
  371. :type diff: boolean
  372. :return: the patch or diff representation of the specified pull-request
  373. :rtype: str
  374. """
  375. repo = flask.g.repo
  376. if not repo.settings.get("pull_requests", True):
  377. flask.abort(404, description="No pull-requests found for this project")
  378. request = pagure.lib.query.search_pull_requests(
  379. flask.g.session, project_id=repo.id, requestid=requestid
  380. )
  381. if not request:
  382. flask.abort(404, description="Pull-request not found")
  383. if request.remote:
  384. repopath = pagure.utils.get_remote_repo_path(
  385. request.remote_git, request.branch_from
  386. )
  387. parentpath = pagure.utils.get_repo_path(request.project)
  388. else:
  389. repo_from = request.project_from
  390. parentpath = pagure.utils.get_repo_path(request.project)
  391. repopath = parentpath
  392. if repo_from:
  393. repopath = pagure.utils.get_repo_path(repo_from)
  394. repo_obj = pygit2.Repository(repopath)
  395. orig_repo = pygit2.Repository(parentpath)
  396. branch = repo_obj.lookup_branch(request.branch_from)
  397. commitid = None
  398. if branch:
  399. commitid = branch.peel().hex
  400. diff_commits = []
  401. if request.status != "Open":
  402. commitid = request.commit_stop
  403. try:
  404. for commit in repo_obj.walk(commitid, pygit2.GIT_SORT_NONE):
  405. diff_commits.append(commit)
  406. if commit.oid.hex == request.commit_start:
  407. break
  408. except KeyError:
  409. # This happens when repo.walk() cannot find commitid
  410. pass
  411. else:
  412. try:
  413. diff_commits = pagure.lib.git.diff_pull_request(
  414. flask.g.session, request, repo_obj, orig_repo, with_diff=False
  415. )
  416. except pagure.exceptions.PagureException as err:
  417. flask.flash("%s" % err, "error")
  418. return flask.redirect(
  419. flask.url_for(
  420. "ui_ns.view_repo",
  421. username=username,
  422. repo=repo.name,
  423. namespace=namespace,
  424. )
  425. )
  426. except SQLAlchemyError as err: # pragma: no cover
  427. flask.g.session.rollback()
  428. _log.exception(err)
  429. flask.flash(
  430. "Could not update this pull-request in the database", "error"
  431. )
  432. diff_commits.reverse()
  433. patch = pagure.lib.git.commit_to_patch(
  434. repo_obj, diff_commits, diff_view=diff
  435. )
  436. return flask.Response(patch, content_type="text/plain;charset=UTF-8")
  437. @UI_NS.route(
  438. "/<repo>/pull-request/<int:requestid>/edit/", methods=("GET", "POST")
  439. )
  440. @UI_NS.route(
  441. "/<repo>/pull-request/<int:requestid>/edit", methods=("GET", "POST")
  442. )
  443. @UI_NS.route(
  444. "/<namespace>/<repo>/pull-request/<int:requestid>/edit/",
  445. methods=("GET", "POST"),
  446. )
  447. @UI_NS.route(
  448. "/<namespace>/<repo>/pull-request/<int:requestid>/edit",
  449. methods=("GET", "POST"),
  450. )
  451. @UI_NS.route(
  452. "/fork/<username>/<repo>/pull-request/<int:requestid>/edit/",
  453. methods=("GET", "POST"),
  454. )
  455. @UI_NS.route(
  456. "/fork/<username>/<repo>/pull-request/<int:requestid>/edit",
  457. methods=("GET", "POST"),
  458. )
  459. @UI_NS.route(
  460. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/edit/",
  461. methods=("GET", "POST"),
  462. )
  463. @UI_NS.route(
  464. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/edit",
  465. methods=("GET", "POST"),
  466. )
  467. @login_required
  468. def request_pull_edit(repo, requestid, username=None, namespace=None):
  469. """Edit the title of a pull-request."""
  470. repo = flask.g.repo
  471. if not repo.settings.get("pull_requests", True):
  472. flask.abort(404, description="No pull-requests found for this project")
  473. request = pagure.lib.query.search_pull_requests(
  474. flask.g.session, project_id=repo.id, requestid=requestid
  475. )
  476. if not request:
  477. flask.abort(404, description="Pull-request not found")
  478. if request.status != "Open":
  479. flask.abort(400, description="Pull-request is already closed")
  480. if (
  481. not flask.g.repo_committer
  482. and flask.g.fas_user.username != request.user.username
  483. ):
  484. flask.abort(
  485. 403, description="You are not allowed to edit this pull-request"
  486. )
  487. form = pagure.forms.RequestPullEditForm(branches=flask.g.branches)
  488. if form.validate_on_submit():
  489. request.title = form.title.data.strip() if form.title.data else None
  490. request.initial_comment = (
  491. form.initial_comment.data.strip()
  492. if form.initial_comment.data
  493. else None
  494. )
  495. request.branch = (
  496. form.branch_to.data.strip() if form.branch_to.data else None
  497. )
  498. if flask.g.fas_user.username == request.user.username:
  499. request.allow_rebase = form.allow_rebase.data
  500. flask.g.session.add(request)
  501. if not request.private and not request.project.private:
  502. pagure.lib.notify.log(
  503. request.project,
  504. topic="pull-request.initial_comment.edited",
  505. msg={
  506. "pullrequest": request.to_json(
  507. public=True, with_comments=False
  508. ),
  509. "project": request.project.to_json(public=True),
  510. "agent": flask.g.fas_user.username,
  511. },
  512. )
  513. try:
  514. # Link the PR to issue(s) if there is such link
  515. pagure.lib.query.link_pr_to_issue_on_description(
  516. flask.g.session, request
  517. )
  518. flask.g.session.commit()
  519. flask.flash("Pull request edited!")
  520. except SQLAlchemyError as err: # pragma: no cover
  521. flask.g.session.rollback()
  522. _log.exception(err)
  523. flask.flash(
  524. "Could not edit this pull-request in the database", "error"
  525. )
  526. return flask.redirect(
  527. flask.url_for(
  528. "ui_ns.request_pull",
  529. username=username,
  530. namespace=namespace,
  531. repo=repo.name,
  532. requestid=requestid,
  533. )
  534. )
  535. elif flask.request.method == "GET":
  536. form.title.data = request.title
  537. form.initial_comment.data = request.initial_comment
  538. form.branch_to.data = request.branch
  539. return flask.render_template(
  540. "pull_request_title.html",
  541. select="requests",
  542. request=request,
  543. repo=repo,
  544. username=username,
  545. form=form,
  546. )
  547. @UI_NS.route("/<repo>/pull-request/<int:requestid>/comment", methods=["POST"])
  548. @UI_NS.route(
  549. "/<repo>/pull-request/<int:requestid>/comment/<commit>/"
  550. "<path:filename>/<row>",
  551. methods=("GET", "POST"),
  552. )
  553. @UI_NS.route(
  554. "/<namespace>/<repo>/pull-request/<int:requestid>/comment",
  555. methods=["POST"],
  556. )
  557. @UI_NS.route(
  558. "/<namespace>/<repo>/pull-request/<int:requestid>/comment/<commit>/"
  559. "<path:filename>/<row>",
  560. methods=("GET", "POST"),
  561. )
  562. @UI_NS.route(
  563. "/fork/<username>/<repo>/pull-request/<int:requestid>/comment",
  564. methods=["POST"],
  565. )
  566. @UI_NS.route(
  567. "/fork/<username>/<repo>/pull-request/<int:requestid>/comment/"
  568. "<commit>/<path:filename>/<row>",
  569. methods=("GET", "POST"),
  570. )
  571. @UI_NS.route(
  572. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/"
  573. "comment",
  574. methods=["POST"],
  575. )
  576. @UI_NS.route(
  577. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/"
  578. "comment/<commit>/<path:filename>/<row>",
  579. methods=("GET", "POST"),
  580. )
  581. @login_required
  582. def pull_request_add_comment(
  583. repo,
  584. requestid,
  585. commit=None,
  586. filename=None,
  587. row=None,
  588. username=None,
  589. namespace=None,
  590. ):
  591. """Add a comment to a commit in a pull-request."""
  592. repo = flask.g.repo
  593. if not repo.settings.get("pull_requests", True):
  594. flask.abort(404, description="No pull-requests found for this project")
  595. request = pagure.lib.query.search_pull_requests(
  596. flask.g.session, project_id=repo.id, requestid=requestid
  597. )
  598. if not request:
  599. flask.abort(404, description="Pull-request not found")
  600. is_js = flask.request.args.get("js", False)
  601. tree_id = flask.request.args.get("tree_id") or None
  602. form = pagure.forms.AddPullRequestCommentForm()
  603. form.commit.data = commit
  604. form.filename.data = filename
  605. form.requestid.data = requestid
  606. form.row.data = row
  607. form.tree_id.data = tree_id
  608. if form.validate_on_submit():
  609. comment = form.comment.data
  610. try:
  611. trigger_ci = pagure_config["TRIGGER_CI"]
  612. if isinstance(trigger_ci, dict):
  613. trigger_ci = list(trigger_ci.keys())
  614. message = pagure.lib.query.add_pull_request_comment(
  615. flask.g.session,
  616. request=request,
  617. commit=commit,
  618. tree_id=tree_id,
  619. filename=filename,
  620. row=row,
  621. comment=comment,
  622. user=flask.g.fas_user.username,
  623. trigger_ci=trigger_ci,
  624. )
  625. flask.g.session.commit()
  626. if not is_js:
  627. flask.flash(message)
  628. except SQLAlchemyError as err: # pragma: no cover
  629. flask.g.session.rollback()
  630. _log.exception(err)
  631. if is_js:
  632. return "error"
  633. else:
  634. flask.flash(str(err), "error")
  635. if is_js:
  636. return "ok"
  637. return flask.redirect(
  638. flask.url_for(
  639. "ui_ns.request_pull",
  640. username=username,
  641. namespace=namespace,
  642. repo=repo.name,
  643. requestid=requestid,
  644. )
  645. )
  646. if is_js and flask.request.method == "POST":
  647. return "failed"
  648. return flask.render_template(
  649. "pull_request_comment.html",
  650. select="requests",
  651. requestid=requestid,
  652. repo=repo,
  653. username=username,
  654. commit=commit,
  655. tree_id=tree_id,
  656. filename=filename,
  657. row=row,
  658. form=form,
  659. )
  660. @UI_NS.route(
  661. "/<repo>/pull-request/<int:requestid>/comment/drop", methods=["POST"]
  662. )
  663. @UI_NS.route(
  664. "/<namespace>/<repo>/pull-request/<int:requestid>/comment/drop",
  665. methods=["POST"],
  666. )
  667. @UI_NS.route(
  668. "/fork/<username>/<repo>/pull-request/<int:requestid>/comment/drop",
  669. methods=["POST"],
  670. )
  671. @UI_NS.route(
  672. "/fork/<namespace>/<username>/<repo>/pull-request/<int:requestid>/"
  673. "comment/drop",
  674. methods=["POST"],
  675. )
  676. @login_required
  677. def pull_request_drop_comment(repo, requestid, username=None, namespace=None):
  678. """Delete a comment of a pull-request."""
  679. repo = flask.g.repo
  680. if not repo:
  681. flask.abort(404, description="Project not found")
  682. if not repo.settings.get("pull_requests", True):
  683. flask.abort(404, description="No pull-requests found for this project")
  684. request = pagure.lib.query.search_pull_requests(
  685. flask.g.session, project_id=repo.id, requestid=requestid
  686. )
  687. if not request:
  688. flask.abort(404, description="Pull-request not found")
  689. if flask.request.form.get("edit_comment"):
  690. commentid = flask.request.form.get("edit_comment")
  691. form = pagure.forms.EditCommentForm()
  692. if form.validate_on_submit():
  693. return pull_request_edit_comment(
  694. repo.name, requestid, commentid, username=username
  695. )
  696. form = pagure.forms.ConfirmationForm()
  697. if form.validate_on_submit():
  698. if flask.request.form.get("drop_comment"):
  699. commentid = flask.request.form.get("drop_comment")
  700. comment = pagure.lib.query.get_request_comment(
  701. flask.g.session, request.uid, commentid
  702. )
  703. if comment is None or comment.pull_request.project != repo:
  704. flask.abort(404, description="Comment not found")
  705. if (
  706. flask.g.fas_user.username != comment.user.username
  707. or comment.parent.status is False
  708. ) and not flask.g.repo_committer:
  709. flask.abort(
  710. 403,
  711. description="You are not allowed to remove this comment "
  712. "from this issue",
  713. )
  714. flask.g.session.delete(comment)
  715. try:
  716. flask.g.session.commit()
  717. flask.flash("Comment removed")
  718. except SQLAlchemyError as err: # pragma: no cover
  719. flask.g.session.rollback()
  720. _log.error(err)
  721. flask.flash(
  722. "Could not remove the comment: %s" % commentid, "error"
  723. )
  724. return flask.redirect(
  725. flask.url_for(
  726. "ui_ns.request_pull",
  727. username=username,
  728. namespace=namespace,
  729. repo=repo.name,
  730. requestid=requestid,
  731. )
  732. )
  733. @UI_NS.route(
  734. "/<repo>/pull-request/<int:requestid>/comment/<int:commentid>/edit",
  735. methods=("GET", "POST"),
  736. )
  737. @UI_NS.route(
  738. "/<namespace>/<repo>/pull-request/<int:requestid>/comment/"
  739. "<int:commentid>/edit",
  740. methods=("GET", "POST"),
  741. )
  742. @UI_NS.route(
  743. "/fork/<username>/<repo>/pull-request/<int:requestid>/comment"
  744. "/<int:commentid>/edit",
  745. methods=("GET", "POST"),
  746. )
  747. @UI_NS.route(
  748. "/fork/<username>/<namespace>/<repo>/pull-request/"
  749. "<int:requestid>/comment/<int:commentid>/edit",
  750. methods=("GET", "POST"),
  751. )
  752. @login_required
  753. def pull_request_edit_comment(
  754. repo, requestid, commentid, username=None, namespace=None
  755. ):
  756. """Edit comment of a pull request"""
  757. is_js = flask.request.args.get("js", False)
  758. project = flask.g.repo
  759. if not project.settings.get("pull_requests", True):
  760. flask.abort(404, description="No pull-requests found for this project")
  761. request = pagure.lib.query.search_pull_requests(
  762. flask.g.session, project_id=project.id, requestid=requestid
  763. )
  764. if not request:
  765. flask.abort(404, description="Pull-request not found")
  766. comment = pagure.lib.query.get_request_comment(
  767. flask.g.session, request.uid, commentid
  768. )
  769. if comment is None or comment.parent.project != project:
  770. flask.abort(404, description="Comment not found")
  771. if (
  772. flask.g.fas_user.username != comment.user.username
  773. or comment.parent.status != "Open"
  774. ) and not flask.g.repo_committer:
  775. flask.abort(403, description="You are not allowed to edit the comment")
  776. form = pagure.forms.EditCommentForm()
  777. if form.validate_on_submit():
  778. updated_comment = form.update_comment.data
  779. try:
  780. message = pagure.lib.query.edit_comment(
  781. flask.g.session,
  782. parent=request,
  783. comment=comment,
  784. user=flask.g.fas_user.username,
  785. updated_comment=updated_comment,
  786. )
  787. flask.g.session.commit()
  788. if not is_js:
  789. flask.flash(message)
  790. except SQLAlchemyError as err: # pragma: no cover
  791. flask.g.session.rollback()
  792. _log.error(err)
  793. if is_js:
  794. return "error"
  795. else:
  796. flask.flash(
  797. "Could not edit the comment: %s" % commentid, "error"
  798. )
  799. if is_js:
  800. return "ok"
  801. return flask.redirect(
  802. flask.url_for(
  803. "ui_ns.request_pull",
  804. username=username,
  805. namespace=namespace,
  806. repo=project.name,
  807. requestid=requestid,
  808. )
  809. )
  810. if is_js and flask.request.method == "POST":
  811. return "failed"
  812. return flask.render_template(
  813. "comment_update.html",
  814. select="requests",
  815. requestid=requestid,
  816. repo=project,
  817. username=username,
  818. form=form,
  819. comment=comment,
  820. is_js=is_js,
  821. )
  822. @UI_NS.route("/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"])
  823. @UI_NS.route(
  824. "/<namespace>/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"]
  825. )
  826. @UI_NS.route(
  827. "/fork/<username>/<repo>/pull-request/<int:requestid>/reopen",
  828. methods=["POST"],
  829. )
  830. @UI_NS.route(
  831. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/reopen",
  832. methods=["POST"],
  833. )
  834. @login_required
  835. def reopen_request_pull(repo, requestid, username=None, namespace=None):
  836. """Re-Open a pull request."""
  837. form = pagure.forms.ConfirmationForm()
  838. if form.validate_on_submit():
  839. if not flask.g.repo.settings.get("pull_requests", True):
  840. flask.abort(
  841. 404, description="No pull-requests found for this project"
  842. )
  843. request = pagure.lib.query.search_pull_requests(
  844. flask.g.session, project_id=flask.g.repo.id, requestid=requestid
  845. )
  846. if not request:
  847. flask.abort(404, description="Pull-request not found")
  848. if (
  849. not flask.g.repo_committer
  850. and not flask.g.fas_user.username == request.user.username
  851. ):
  852. flask.abort(
  853. 403,
  854. description="You are not allowed to reopen pull-request "
  855. "for this project",
  856. )
  857. try:
  858. pagure.lib.query.reopen_pull_request(
  859. flask.g.session, request, flask.g.fas_user.username
  860. )
  861. except pagure.exceptions.PagureException as err:
  862. flask.flash(str(err), "error")
  863. try:
  864. flask.g.session.commit()
  865. flask.flash("Pull request reopened!")
  866. except SQLAlchemyError as err: # pragma: no cover
  867. flask.g.session.rollback()
  868. _log.exception(err)
  869. flask.flash(
  870. "Could not update this pull-request in the database", "error"
  871. )
  872. else:
  873. flask.flash("Invalid input submitted", "error")
  874. return flask.redirect(
  875. flask.url_for(
  876. "ui_ns.request_pull",
  877. repo=repo,
  878. username=username,
  879. namespace=namespace,
  880. requestid=requestid,
  881. )
  882. )
  883. @UI_NS.route(
  884. "/<repo>/pull-request/<int:requestid>/trigger-ci", methods=["POST"]
  885. )
  886. @UI_NS.route(
  887. "/<namespace>/<repo>/pull-request/<int:requestid>/trigger-ci",
  888. methods=["POST"],
  889. )
  890. @UI_NS.route(
  891. "/fork/<username>/<repo>/pull-request/<int:requestid>/trigger-ci",
  892. methods=["POST"],
  893. )
  894. @UI_NS.route(
  895. (
  896. "/fork/<username>/<namespace>/<repo>/pull-request/"
  897. "<int:requestid>/trigger-ci"
  898. ),
  899. methods=["POST"],
  900. )
  901. @login_required
  902. def ci_trigger_request_pull(repo, requestid, username=None, namespace=None):
  903. """Trigger CI testing for a PR."""
  904. form = pagure.forms.TriggerCIPRForm()
  905. if not form.validate_on_submit():
  906. flask.flash("Invalid input submitted", "error")
  907. return flask.redirect(
  908. flask.url_for(
  909. "ui_ns.request_pull",
  910. repo=repo,
  911. requestid=requestid,
  912. username=username,
  913. namespace=namespace,
  914. )
  915. )
  916. repo_obj = flask.g.repo
  917. request = pagure.lib.query.search_pull_requests(
  918. flask.g.session, project_id=repo_obj.id, requestid=requestid
  919. )
  920. if not request:
  921. flask.abort(404, description="Pull-request not found")
  922. trigger_ci = pagure_config["TRIGGER_CI"]
  923. if isinstance(trigger_ci, dict):
  924. trigger_ci = list(trigger_ci.keys())
  925. pagure.lib.query.add_pull_request_comment(
  926. flask.g.session,
  927. request,
  928. commit=None,
  929. tree_id=None,
  930. filename=None,
  931. row=None,
  932. comment=form.comment.data,
  933. user=flask.g.fas_user.username,
  934. notify=True,
  935. notification=True,
  936. trigger_ci=trigger_ci,
  937. )
  938. return flask.redirect(
  939. flask.url_for(
  940. "ui_ns.request_pull",
  941. repo=repo,
  942. username=username,
  943. namespace=namespace,
  944. requestid=requestid,
  945. )
  946. )
  947. @UI_NS.route("/<repo>/pull-request/<int:requestid>/merge", methods=["POST"])
  948. @UI_NS.route(
  949. "/<namespace>/<repo>/pull-request/<int:requestid>/merge", methods=["POST"]
  950. )
  951. @UI_NS.route(
  952. "/fork/<username>/<repo>/pull-request/<int:requestid>/merge",
  953. methods=["POST"],
  954. )
  955. @UI_NS.route(
  956. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/merge",
  957. methods=["POST"],
  958. )
  959. @login_required
  960. def merge_request_pull(repo, requestid, username=None, namespace=None):
  961. """Create a pull request with the changes from the fork into the project."""
  962. form = pagure.forms.MergePRForm()
  963. if not form.validate_on_submit():
  964. flask.flash("Invalid input submitted", "error")
  965. return flask.redirect(
  966. flask.url_for(
  967. "ui_ns.request_pull",
  968. repo=repo,
  969. requestid=requestid,
  970. username=username,
  971. namespace=namespace,
  972. )
  973. )
  974. repo = flask.g.repo
  975. _log.info(
  976. "called merge_request_pull for repo: %s - requestid: %s",
  977. repo.fullname,
  978. requestid,
  979. )
  980. if not repo.settings.get("pull_requests", True):
  981. flask.abort(404, description="No pull-requests found for this project")
  982. request = pagure.lib.query.search_pull_requests(
  983. flask.g.session, project_id=repo.id, requestid=requestid
  984. )
  985. if not request:
  986. flask.abort(404, description="Pull-request not found")
  987. if not flask.g.repo_committer:
  988. flask.abort(
  989. 403,
  990. description="You are not allowed to merge pull-request "
  991. "for this project",
  992. )
  993. if repo.settings.get("Only_assignee_can_merge_pull-request", False):
  994. if not request.assignee:
  995. flask.flash("This request must be assigned to be merged", "error")
  996. return flask.redirect(
  997. flask.url_for(
  998. "ui_ns.request_pull",
  999. username=username,
  1000. namespace=namespace,
  1001. repo=repo.name,
  1002. requestid=requestid,
  1003. )
  1004. )
  1005. if request.assignee.username != flask.g.fas_user.username:
  1006. flask.flash("Only the assignee can merge this request", "error")
  1007. return flask.redirect(
  1008. flask.url_for(
  1009. "ui_ns.request_pull",
  1010. username=username,
  1011. namespace=namespace,
  1012. repo=repo.name,
  1013. requestid=requestid,
  1014. )
  1015. )
  1016. threshold = repo.settings.get("Minimum_score_to_merge_pull-request", -1)
  1017. if threshold > 0 and int(request.score) < int(threshold):
  1018. flask.flash(
  1019. "This request does not have the minimum review score necessary "
  1020. "to be merged",
  1021. "error",
  1022. )
  1023. return flask.redirect(
  1024. flask.url_for(
  1025. "ui_ns.request_pull",
  1026. username=username,
  1027. namespace=namespace,
  1028. repo=repo.name,
  1029. requestid=requestid,
  1030. )
  1031. )
  1032. if form.delete_branch.data:
  1033. if not pagure_config.get("ALLOW_DELETE_BRANCH", True):
  1034. flask.flash(
  1035. "This pagure instance does not allow branch deletion", "error"
  1036. )
  1037. return flask.redirect(
  1038. flask.url_for(
  1039. "ui_ns.request_pull",
  1040. username=username,
  1041. namespace=namespace,
  1042. repo=repo.name,
  1043. requestid=requestid,
  1044. )
  1045. )
  1046. committer = False
  1047. if request.project_from:
  1048. committer = pagure.utils.is_repo_committer(request.project_from)
  1049. else:
  1050. committer = pagure.utils.is_repo_committer(request.project)
  1051. if not committer:
  1052. flask.flash(
  1053. "You do not have permissions to delete the branch in the "
  1054. "source repo",
  1055. "error",
  1056. )
  1057. return flask.redirect(
  1058. flask.url_for(
  1059. "ui_ns.request_pull",
  1060. username=username,
  1061. namespace=namespace,
  1062. repo=repo.name,
  1063. requestid=requestid,
  1064. )
  1065. )
  1066. if request.remote_git:
  1067. flask.flash("You can not delete branch in remote repo", "error")
  1068. return flask.redirect(
  1069. flask.url_for(
  1070. "ui_ns.request_pull",
  1071. username=username,
  1072. namespace=namespace,
  1073. repo=repo.name,
  1074. requestid=requestid,
  1075. )
  1076. )
  1077. _log.info("All checks in the controller passed")
  1078. try:
  1079. if flask.request.form.get("comment"):
  1080. trigger_ci = pagure_config["TRIGGER_CI"]
  1081. if isinstance(trigger_ci, dict):
  1082. trigger_ci = list(trigger_ci.keys())
  1083. message = pagure.lib.query.add_pull_request_comment(
  1084. flask.g.session,
  1085. request=request,
  1086. commit=None,
  1087. tree_id=None,
  1088. filename=None,
  1089. row=None,
  1090. comment=flask.request.form.get("comment"),
  1091. user=flask.g.fas_user.username,
  1092. trigger_ci=trigger_ci,
  1093. )
  1094. flask.g.session.commit()
  1095. flask.flash(message)
  1096. task = pagure.lib.tasks.merge_pull_request.delay(
  1097. repo.name,
  1098. namespace,
  1099. username,
  1100. requestid,
  1101. flask.g.fas_user.username,
  1102. delete_branch_after=form.delete_branch.data,
  1103. )
  1104. return pagure.utils.wait_for_task(
  1105. task,
  1106. prev=flask.url_for(
  1107. "ui_ns.request_pull",
  1108. repo=repo.name,
  1109. namespace=namespace,
  1110. username=username,
  1111. requestid=requestid,
  1112. ),
  1113. )
  1114. except SQLAlchemyError as err: # pragma: no cover
  1115. flask.g.session.rollback()
  1116. _log.exception(err)
  1117. flask.flash(str(err), "error")
  1118. return flask.redirect(
  1119. flask.url_for(
  1120. "ui_ns.request_pull",
  1121. repo=repo.name,
  1122. requestid=requestid,
  1123. username=username,
  1124. namespace=namespace,
  1125. )
  1126. )
  1127. except pygit2.GitError as err:
  1128. _log.info("GitError exception raised")
  1129. flask.flash("%s" % err, "error")
  1130. return flask.redirect(
  1131. flask.url_for(
  1132. "ui_ns.request_pull",
  1133. repo=repo.name,
  1134. requestid=requestid,
  1135. username=username,
  1136. namespace=namespace,
  1137. )
  1138. )
  1139. except pagure.exceptions.PagureException as err:
  1140. _log.info("PagureException exception raised")
  1141. flask.flash(str(err), "error")
  1142. return flask.redirect(
  1143. flask.url_for(
  1144. "ui_ns.request_pull",
  1145. repo=repo.name,
  1146. requestid=requestid,
  1147. username=username,
  1148. namespace=namespace,
  1149. )
  1150. )
  1151. _log.info("All fine, returning")
  1152. return flask.redirect(
  1153. flask.url_for(
  1154. "ui_ns.view_repo",
  1155. repo=repo.name,
  1156. username=username,
  1157. namespace=namespace,
  1158. )
  1159. )
  1160. @UI_NS.route("/<repo>/pull-request/close/<int:requestid>", methods=["POST"])
  1161. @UI_NS.route(
  1162. "/<namespace>/<repo>/pull-request/close/<int:requestid>", methods=["POST"]
  1163. )
  1164. @UI_NS.route(
  1165. "/fork/<username>/<repo>/pull-request/close/<int:requestid>",
  1166. methods=["POST"],
  1167. )
  1168. @UI_NS.route(
  1169. "/fork/<username>/<namespace>/<repo>/pull-request/close/<int:requestid>",
  1170. methods=["POST"],
  1171. )
  1172. @login_required
  1173. def close_request_pull(repo, requestid, username=None, namespace=None):
  1174. """Close a pull request without merging it."""
  1175. form = pagure.forms.ConfirmationForm()
  1176. if form.validate_on_submit():
  1177. if not flask.g.repo.settings.get("pull_requests", True):
  1178. flask.abort(
  1179. 404, description="No pull-requests found for this project"
  1180. )
  1181. request = pagure.lib.query.search_pull_requests(
  1182. flask.g.session, project_id=flask.g.repo.id, requestid=requestid
  1183. )
  1184. if not request:
  1185. flask.abort(404, description="Pull-request not found")
  1186. if (
  1187. not flask.g.repo_committer
  1188. and not flask.g.fas_user.username == request.user.username
  1189. ):
  1190. flask.abort(
  1191. 403,
  1192. description="You are not allowed to close pull-request "
  1193. "for this project",
  1194. )
  1195. pagure.lib.query.close_pull_request(
  1196. flask.g.session, request, flask.g.fas_user.username, merged=False
  1197. )
  1198. try:
  1199. flask.g.session.commit()
  1200. flask.flash("Pull request closed!")
  1201. except SQLAlchemyError as err: # pragma: no cover
  1202. flask.g.session.rollback()
  1203. _log.exception(err)
  1204. flask.flash(
  1205. "Could not update this pull-request in the database", "error"
  1206. )
  1207. else:
  1208. flask.flash("Invalid input submitted", "error")
  1209. return flask.redirect(
  1210. flask.url_for(
  1211. "ui_ns.view_repo",
  1212. repo=repo,
  1213. username=username,
  1214. namespace=namespace,
  1215. )
  1216. )
  1217. @UI_NS.route("/<repo>/pull-request/refresh/<int:requestid>", methods=["POST"])
  1218. @UI_NS.route(
  1219. "/<namespace>/<repo>/pull-request/refresh/<int:requestid>",
  1220. methods=["POST"],
  1221. )
  1222. @UI_NS.route(
  1223. "/fork/<username>/<repo>/pull-request/refresh/<int:requestid>",
  1224. methods=["POST"],
  1225. )
  1226. @UI_NS.route(
  1227. "/fork/<username>/<namespace>/<repo>/pull-request/refresh/<int:requestid>",
  1228. methods=["POST"],
  1229. )
  1230. @login_required
  1231. def refresh_request_pull(repo, requestid, username=None, namespace=None):
  1232. """Refresh a remote pull request."""
  1233. form = pagure.forms.ConfirmationForm()
  1234. if form.validate_on_submit():
  1235. if not flask.g.repo.settings.get("pull_requests", True):
  1236. flask.abort(
  1237. 404, description="No pull-requests found for this project"
  1238. )
  1239. request = pagure.lib.query.search_pull_requests(
  1240. flask.g.session, project_id=flask.g.repo.id, requestid=requestid
  1241. )
  1242. if not request:
  1243. flask.abort(404, description="Pull-request not found")
  1244. if (
  1245. not flask.g.repo_committer
  1246. and not flask.g.fas_user.username == request.user.username
  1247. ):
  1248. flask.abort(
  1249. 403,
  1250. description="You are not allowed to refresh this pull request",
  1251. )
  1252. task = pagure.lib.tasks.refresh_remote_pr.delay(
  1253. flask.g.repo.name, namespace, username, requestid
  1254. )
  1255. return pagure.utils.wait_for_task(
  1256. task,
  1257. prev=flask.url_for(
  1258. "ui_ns.request_pull",
  1259. repo=flask.g.repo.name,
  1260. namespace=namespace,
  1261. username=username,
  1262. requestid=requestid,
  1263. ),
  1264. )
  1265. else:
  1266. flask.flash("Invalid input submitted", "error")
  1267. return flask.redirect(
  1268. flask.url_for(
  1269. "ui_ns.request_pull",
  1270. username=username,
  1271. namespace=namespace,
  1272. repo=flask.g.repo.name,
  1273. requestid=requestid,
  1274. )
  1275. )
  1276. @UI_NS.route("/<repo>/pull-request/<int:requestid>/update", methods=["POST"])
  1277. @UI_NS.route(
  1278. "/<namespace>/<repo>/pull-request/<int:requestid>/update", methods=["POST"]
  1279. )
  1280. @UI_NS.route(
  1281. "/fork/<username>/<repo>/pull-request/<int:requestid>/update",
  1282. methods=["POST"],
  1283. )
  1284. @UI_NS.route(
  1285. "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/update",
  1286. methods=["POST"],
  1287. )
  1288. @login_required
  1289. def update_pull_requests(repo, requestid, username=None, namespace=None):
  1290. """Update the metadata of a pull-request."""
  1291. repo = flask.g.repo
  1292. if not repo.settings.get("pull_requests", True):
  1293. flask.abort(404, description="No pull-request allowed on this project")
  1294. request = pagure.lib.query.search_pull_requests(
  1295. flask.g.session, project_id=repo.id, requestid=requestid
  1296. )
  1297. if not request:
  1298. flask.abort(404, description="Pull-request not found")
  1299. if (
  1300. not flask.g.repo_user
  1301. and flask.g.fas_user.username != request.user.username
  1302. ):
  1303. flask.abort(
  1304. 403, description="You are not allowed to update this pull-request"
  1305. )
  1306. form = pagure.forms.ConfirmationForm()
  1307. if form.validate_on_submit():
  1308. tags = [
  1309. tag.strip()
  1310. for tag in flask.request.form.get("tag", "").strip().split(",")
  1311. if tag.strip()
  1312. ]
  1313. messages = set()
  1314. try:
  1315. # Adjust (add/remove) tags
  1316. msgs = pagure.lib.query.update_tags(
  1317. flask.g.session,
  1318. obj=request,
  1319. tags=tags,
  1320. username=flask.g.fas_user.username,
  1321. )
  1322. messages = messages.union(set(msgs))
  1323. if flask.g.repo_user:
  1324. # Assign or update assignee of the ticket
  1325. msg = pagure.lib.query.add_pull_request_assignee(
  1326. flask.g.session,
  1327. request=request,
  1328. assignee=flask.request.form.get("user", "").strip()
  1329. or None,
  1330. user=flask.g.fas_user.username,
  1331. )
  1332. if msg:
  1333. messages.add(msg)
  1334. if messages:
  1335. # Add the comment for field updates:
  1336. not_needed = set(["Comment added", "Updated comment"])
  1337. pagure.lib.query.add_metadata_update_notif(
  1338. session=flask.g.session,
  1339. obj=request,
  1340. messages=messages - not_needed,
  1341. user=flask.g.fas_user.username,
  1342. )
  1343. messages.add("Metadata fields updated")
  1344. flask.g.session.commit()
  1345. for message in messages:
  1346. flask.flash(message)
  1347. except pagure.exceptions.PagureException as err:
  1348. flask.g.session.rollback()
  1349. flask.flash("%s" % err, "error")
  1350. except SQLAlchemyError as err: # pragma: no cover
  1351. flask.g.session.rollback()
  1352. _log.exception(err)
  1353. flask.flash(str(err), "error")
  1354. return flask.redirect(
  1355. flask.url_for(
  1356. "ui_ns.request_pull",
  1357. username=username,
  1358. namespace=namespace,
  1359. repo=repo.name,
  1360. requestid=requestid,
  1361. )
  1362. )
  1363. # Specific actions
  1364. @UI_NS.route("/do_fork/<repo>", methods=["POST"])
  1365. @UI_NS.route("/do_fork/<namespace>/<repo>", methods=["POST"])
  1366. @UI_NS.route("/do_fork/fork/<username>/<repo>", methods=["POST"])
  1367. @UI_NS.route("/do_fork/fork/<username>/<namespace>/<repo>", methods=["POST"])
  1368. @login_required
  1369. def fork_project(repo, username=None, namespace=None):
  1370. """Fork the project specified into the user's namespace"""
  1371. repo = flask.g.repo
  1372. form = pagure.forms.ConfirmationForm()
  1373. if not form.validate_on_submit():
  1374. flask.abort(400)
  1375. if pagure.lib.query._get_project(
  1376. flask.g.session,
  1377. repo.name,
  1378. user=flask.g.fas_user.username,
  1379. namespace=namespace,
  1380. ):
  1381. return flask.redirect(
  1382. flask.url_for(
  1383. "ui_ns.view_repo",
  1384. repo=repo.name,
  1385. username=flask.g.fas_user.username,
  1386. namespace=namespace,
  1387. )
  1388. )
  1389. try:
  1390. task = pagure.lib.query.fork_project(
  1391. session=flask.g.session, repo=repo, user=flask.g.fas_user.username
  1392. )
  1393. flask.g.session.commit()
  1394. return pagure.utils.wait_for_task(
  1395. task,
  1396. prev=flask.url_for(
  1397. "ui_ns.view_repo",
  1398. repo=repo.name,
  1399. username=username,
  1400. namespace=namespace,
  1401. _external=True,
  1402. ),
  1403. )
  1404. except pagure.exceptions.PagureException as err:
  1405. flask.flash(str(err), "error")
  1406. except SQLAlchemyError as err: # pragma: no cover
  1407. flask.g.session.rollback()
  1408. flask.flash(str(err), "error")
  1409. return flask.redirect(
  1410. flask.url_for(
  1411. "ui_ns.view_repo",
  1412. repo=repo.name,
  1413. username=username,
  1414. namespace=namespace,
  1415. )
  1416. )
  1417. @UI_NS.route(
  1418. "/<repo>/diff/<path:branch_to>..<path:branch_from>/",
  1419. methods=("GET", "POST"),
  1420. )
  1421. @UI_NS.route(
  1422. "/<repo>/diff/<path:branch_to>..<path:branch_from>",
  1423. methods=("GET", "POST"),
  1424. )
  1425. @UI_NS.route(
  1426. "/<namespace>/<repo>/diff/<path:branch_to>..<path:branch_from>/",
  1427. methods=("GET", "POST"),
  1428. )
  1429. @UI_NS.route(
  1430. "/<namespace>/<repo>/diff/<path:branch_to>..<path:branch_from>",
  1431. methods=("GET", "POST"),
  1432. )
  1433. @UI_NS.route(
  1434. "/fork/<username>/<repo>/diff/<path:branch_to>..<path:branch_from>/",
  1435. methods=("GET", "POST"),
  1436. )
  1437. @UI_NS.route(
  1438. "/fork/<username>/<repo>/diff/<path:branch_to>..<path:branch_from>",
  1439. methods=("GET", "POST"),
  1440. )
  1441. @UI_NS.route(
  1442. "/fork/<username>/<namespace>/<repo>/diff/"
  1443. "<path:branch_to>..<path:branch_from>/",
  1444. methods=("GET", "POST"),
  1445. )
  1446. @UI_NS.route(
  1447. "/fork/<username>/<namespace>/<repo>/diff/"
  1448. "<path:branch_to>..<path:branch_from>",
  1449. methods=("GET", "POST"),
  1450. )
  1451. def new_request_pull(
  1452. repo, branch_to, branch_from, username=None, namespace=None
  1453. ):
  1454. """Create a pull request with the changes from the fork into the project."""
  1455. branch_to = flask.request.values.get("branch_to", branch_to)
  1456. project_to = flask.request.values.get("project_to")
  1457. repo = flask.g.repo
  1458. parent = repo
  1459. if repo.parent:
  1460. parent = repo.parent
  1461. repo_obj = flask.g.repo_obj
  1462. if not project_to:
  1463. parentpath = get_parent_repo_path(repo)
  1464. orig_repo = pygit2.Repository(parentpath)
  1465. else:
  1466. p_namespace = None
  1467. p_username = None
  1468. p_name = None
  1469. project_to = project_to.rstrip("/")
  1470. if project_to.startswith("fork/"):
  1471. tmp = project_to.split("fork/")[1]
  1472. p_username, left = tmp.split("/", 1)
  1473. else:
  1474. left = project_to
  1475. if "/" in left:
  1476. p_namespace, p_name = left.split("/", 1)
  1477. else:
  1478. p_name = left
  1479. parent = pagure.lib.query.get_authorized_project(
  1480. flask.g.session, p_name, user=p_username, namespace=p_namespace
  1481. )
  1482. if parent:
  1483. family = [
  1484. p.url_path
  1485. for p in pagure.lib.query.get_project_family(
  1486. flask.g.session, repo
  1487. )
  1488. ]
  1489. if parent.url_path not in family:
  1490. flask.abort(
  1491. 400,
  1492. description="%s is not part of %s's family"
  1493. % (project_to, repo.url_path),
  1494. )
  1495. orig_repo = pygit2.Repository(parent.repopath("main"))
  1496. else:
  1497. flask.abort(
  1498. 404, description="No project found for %s" % project_to
  1499. )
  1500. if not parent.settings.get("pull_requests", True):
  1501. flask.abort(404, description="No pull-request allowed on this project")
  1502. if parent.settings.get(
  1503. "Enforce_signed-off_commits_in_pull-request", False
  1504. ):
  1505. flask.flash(
  1506. "This project enforces the Signed-off-by statement on all "
  1507. "commits"
  1508. )
  1509. try:
  1510. diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info(
  1511. repo_obj, orig_repo, branch_from, branch_to
  1512. )
  1513. except pagure.exceptions.PagureException as err:
  1514. flask.abort(400, description=str(err))
  1515. repo_committer = flask.g.repo_committer
  1516. form = pagure.forms.RequestPullForm()
  1517. if form.validate_on_submit() and repo_committer:
  1518. try:
  1519. if parent.settings.get(
  1520. "Enforce_signed-off_commits_in_pull-request", False
  1521. ):
  1522. for commit in diff_commits:
  1523. if "signed-off-by" not in commit.message.lower():
  1524. raise pagure.exceptions.PagureException(
  1525. "This repo enforces that all commits are "
  1526. "signed off by their author. "
  1527. )
  1528. if orig_commit:
  1529. orig_commit = orig_commit.oid.hex
  1530. initial_comment = (
  1531. form.initial_comment.data.strip()
  1532. if form.initial_comment.data
  1533. else None
  1534. )
  1535. commit_start = commit_stop = None
  1536. if diff_commits:
  1537. commit_stop = diff_commits[0].oid.hex
  1538. commit_start = diff_commits[-1].oid.hex
  1539. request = pagure.lib.query.new_pull_request(
  1540. flask.g.session,
  1541. repo_to=parent,
  1542. branch_to=branch_to,
  1543. branch_from=branch_from,
  1544. repo_from=repo,
  1545. title=form.title.data,
  1546. initial_comment=initial_comment,
  1547. allow_rebase=form.allow_rebase.data,
  1548. user=flask.g.fas_user.username,
  1549. commit_start=commit_start,
  1550. commit_stop=commit_stop,
  1551. )
  1552. try:
  1553. flask.g.session.commit()
  1554. except SQLAlchemyError as err: # pragma: no cover
  1555. flask.g.session.rollback()
  1556. _log.exception(err)
  1557. flask.flash(
  1558. "Could not register this pull-request in the database",
  1559. "error",
  1560. )
  1561. if not parent.is_fork:
  1562. url = flask.url_for(
  1563. "ui_ns.request_pull",
  1564. requestid=request.id,
  1565. username=None,
  1566. repo=parent.name,
  1567. namespace=namespace,
  1568. )
  1569. else:
  1570. url = flask.url_for(
  1571. "ui_ns.request_pull",
  1572. requestid=request.id,
  1573. username=parent.user.user,
  1574. repo=parent.name,
  1575. namespace=namespace,
  1576. )
  1577. return flask.redirect(url)
  1578. except pagure.exceptions.PagureException as err: # pragma: no cover
  1579. # There could be a PagureException thrown if the flask.g.fas_user
  1580. # wasn't in the DB but then it shouldn't be recognized as a
  1581. # repo admin and thus, if we ever are here, we are in trouble.
  1582. flask.flash(str(err), "error")
  1583. except SQLAlchemyError as err: # pragma: no cover
  1584. flask.g.session.rollback()
  1585. flask.flash(str(err), "error")
  1586. if not flask.g.repo_committer:
  1587. form = None
  1588. elif flask.request.method == "GET":
  1589. form.allow_rebase.data = True
  1590. # if the pull request we are creating only has one commit,
  1591. # we automatically fill out the form fields for the PR with
  1592. # the commit title and bodytext
  1593. if len(diff_commits) == 1 and form:
  1594. form.title.data = diff_commits[0].message.strip().split("\n")[0]
  1595. form.initial_comment.data = diff_commits[0].message.partition("\n")[2]
  1596. # Get the contributing templates from the requests git repo
  1597. contributing = None
  1598. requestrepopath = _get_parent_request_repo_path(repo)
  1599. if os.path.exists(requestrepopath):
  1600. requestrepo = pygit2.Repository(requestrepopath)
  1601. if not requestrepo.is_empty and not requestrepo.head_is_unborn:
  1602. commit = requestrepo[requestrepo.head.target]
  1603. contributing = __get_file_in_tree(
  1604. requestrepo,
  1605. commit.tree,
  1606. ["templates", "contributing.md"],
  1607. bail_on_tree=True,
  1608. )
  1609. if contributing:
  1610. contributing, _ = pagure.doc_utils.convert_readme(
  1611. contributing.data, "md"
  1612. )
  1613. flask.g.branches = sorted(orig_repo.listall_branches())
  1614. if diff:
  1615. diff.find_similar()
  1616. return flask.render_template(
  1617. "repo_new_pull_request.html",
  1618. select="requests",
  1619. repo=repo,
  1620. username=username,
  1621. orig_repo=orig_repo,
  1622. parent_branches=sorted(flask.g.repo_obj.listall_branches()),
  1623. diff_commits=diff_commits,
  1624. diff=diff,
  1625. form=form,
  1626. branch_to=branch_to,
  1627. branch_from=branch_from,
  1628. contributing=contributing,
  1629. parent=parent,
  1630. project_to=project_to,
  1631. )
  1632. @UI_NS.route("/<repo>/diff/remote/", methods=("GET", "POST"))
  1633. @UI_NS.route("/<repo>/diff/remote", methods=("GET", "POST"))
  1634. @UI_NS.route("/<namespace>/<repo>/diff/remote/", methods=("GET", "POST"))
  1635. @UI_NS.route("/<namespace>/<repo>/diff/remote", methods=("GET", "POST"))
  1636. @UI_NS.route("/fork/<username>/<repo>/diff/remote/", methods=("GET", "POST"))
  1637. @UI_NS.route("/fork/<username>/<repo>/diff/remote", methods=("GET", "POST"))
  1638. @UI_NS.route(
  1639. "/fork/<username>/<namespace>/<repo>/diff/remote/", methods=("GET", "POST")
  1640. )
  1641. @UI_NS.route(
  1642. "/fork/<username>/<namespace>/<repo>/diff/remote", methods=("GET", "POST")
  1643. )
  1644. @login_required
  1645. def new_remote_request_pull(repo, username=None, namespace=None):
  1646. """Create a pull request with the changes from a remote fork into the
  1647. project.
  1648. """
  1649. confirm = flask.request.values.get("confirm", False)
  1650. repo = flask.g.repo
  1651. if pagure_config.get("DISABLE_REMOTE_PR", True):
  1652. flask.abort(
  1653. 404, description="Remote pull-requests disabled on this server"
  1654. )
  1655. if not repo.settings.get("pull_requests", True):
  1656. flask.abort(404, description="No pull-request allowed on this project")
  1657. if repo.settings.get("Enforce_signed-off_commits_in_pull-request", False):
  1658. flask.flash(
  1659. "This project enforces the Signed-off-by statement on all "
  1660. "commits"
  1661. )
  1662. orig_repo = flask.g.repo_obj
  1663. form = pagure.forms.RemoteRequestPullForm()
  1664. if form.validate_on_submit():
  1665. taskid = flask.request.values.get("taskid")
  1666. if taskid:
  1667. result = pagure.lib.tasks.get_result(taskid)
  1668. if not result.ready:
  1669. return pagure.utils.wait_for_task_post(
  1670. taskid,
  1671. form,
  1672. "ui_ns.new_remote_request_pull",
  1673. repo=repo.name,
  1674. username=username,
  1675. namespace=namespace,
  1676. )
  1677. # Make sure to collect any exceptions resulting from the task
  1678. try:
  1679. result.get(timeout=0)
  1680. except Exception as err:
  1681. flask.abort(500, description=err)
  1682. branch_from = (
  1683. form.branch_from.data.strip() if form.branch_from.data else None
  1684. )
  1685. branch_to = (
  1686. form.branch_to.data.strip() if form.branch_to.data else None
  1687. )
  1688. remote_git = form.git_repo.data.strip() if form.git_repo.data else None
  1689. repopath = pagure.utils.get_remote_repo_path(remote_git, branch_from)
  1690. if not repopath:
  1691. taskid = pagure.lib.tasks.pull_remote_repo.delay(
  1692. remote_git, branch_from
  1693. )
  1694. return pagure.utils.wait_for_task_post(
  1695. taskid,
  1696. form,
  1697. "ui_ns.new_remote_request_pull",
  1698. repo=repo.name,
  1699. username=username,
  1700. namespace=namespace,
  1701. initial=True,
  1702. )
  1703. repo_obj = pygit2.Repository(repopath)
  1704. try:
  1705. diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info(
  1706. repo_obj, orig_repo, branch_from, branch_to
  1707. )
  1708. except pagure.exceptions.PagureException as err:
  1709. flask.flash("%s" % err, "error")
  1710. return flask.redirect(
  1711. flask.url_for(
  1712. "ui_ns.view_repo",
  1713. username=username,
  1714. repo=repo.name,
  1715. namespace=namespace,
  1716. )
  1717. )
  1718. if not confirm:
  1719. flask.g.branches = sorted(orig_repo.listall_branches())
  1720. return flask.render_template(
  1721. "repo_new_pull_request.html",
  1722. select="requests",
  1723. repo=repo,
  1724. username=username,
  1725. orig_repo=orig_repo,
  1726. diff_commits=diff_commits,
  1727. diff=diff,
  1728. form=form,
  1729. branch_to=branch_to,
  1730. branch_from=branch_from,
  1731. remote_git=remote_git,
  1732. parent=repo,
  1733. )
  1734. try:
  1735. if repo.settings.get(
  1736. "Enforce_signed-off_commits_in_pull-request", False
  1737. ):
  1738. for commit in diff_commits:
  1739. if "signed-off-by" not in commit.message.lower():
  1740. raise pagure.exceptions.PagureException(
  1741. "This repo enforces that all commits are "
  1742. "signed off by their author. "
  1743. )
  1744. if orig_commit:
  1745. orig_commit = orig_commit.oid.hex
  1746. parent = repo
  1747. if repo.parent:
  1748. parent = repo.parent
  1749. request = pagure.lib.query.new_pull_request(
  1750. flask.g.session,
  1751. repo_to=parent,
  1752. branch_to=branch_to,
  1753. branch_from=branch_from,
  1754. repo_from=None,
  1755. remote_git=remote_git,
  1756. title=form.title.data,
  1757. user=flask.g.fas_user.username,
  1758. )
  1759. if (
  1760. form.initial_comment.data
  1761. and form.initial_comment.data.strip() != ""
  1762. ):
  1763. pagure.lib.query.add_pull_request_comment(
  1764. flask.g.session,
  1765. request=request,
  1766. commit=None,
  1767. tree_id=None,
  1768. filename=None,
  1769. row=None,
  1770. comment=form.initial_comment.data.strip(),
  1771. user=flask.g.fas_user.username,
  1772. )
  1773. try:
  1774. flask.g.session.commit()
  1775. flask.flash("Request created")
  1776. except SQLAlchemyError as err: # pragma: no cover
  1777. flask.g.session.rollback()
  1778. _log.exception(err)
  1779. flask.flash(
  1780. "Could not register this pull-request in " "the database",
  1781. "error",
  1782. )
  1783. if not parent.is_fork:
  1784. url = flask.url_for(
  1785. "ui_ns.request_pull",
  1786. requestid=request.id,
  1787. username=None,
  1788. repo=parent.name,
  1789. namespace=namespace,
  1790. )
  1791. else:
  1792. url = flask.url_for(
  1793. "ui_ns.request_pull",
  1794. requestid=request.id,
  1795. username=parent.user,
  1796. repo=parent.name,
  1797. namespace=namespace,
  1798. )
  1799. return flask.redirect(url)
  1800. except pagure.exceptions.PagureException as err: # pragma: no cover
  1801. # There could be a PagureException thrown if the
  1802. # flask.g.fas_user wasn't in the DB but then it shouldn't
  1803. # be recognized as a repo admin and thus, if we ever are
  1804. # here, we are in trouble.
  1805. flask.flash(str(err), "error")
  1806. except SQLAlchemyError as err: # pragma: no cover
  1807. flask.g.session.rollback()
  1808. flask.flash(str(err), "error")
  1809. flask.g.branches = sorted(orig_repo.listall_branches())
  1810. if flask.request.method == "GET":
  1811. try:
  1812. branch_to = orig_repo.head.shorthand
  1813. except pygit2.GitError:
  1814. branch_to = "master"
  1815. else:
  1816. branch_to = (
  1817. form.branch_to.data.strip() if form.branch_to.data else None
  1818. )
  1819. return flask.render_template(
  1820. "remote_pull_request.html",
  1821. select="requests",
  1822. repo=repo,
  1823. username=username,
  1824. form=form,
  1825. branch_to=branch_to,
  1826. )
  1827. @UI_NS.route(
  1828. "/fork_edit/<repo>/edit/<path:branchname>/f/<path:filename>",
  1829. methods=["POST"],
  1830. )
  1831. @UI_NS.route(
  1832. "/fork_edit/<namespace>/<repo>/edit/<path:branchname>/f/<path:filename>",
  1833. methods=["POST"],
  1834. )
  1835. @UI_NS.route(
  1836. "/fork_edit/fork/<username>/<repo>/edit/<path:branchname>/"
  1837. "f/<path:filename>",
  1838. methods=["POST"],
  1839. )
  1840. @UI_NS.route(
  1841. "/fork_edit/fork/<username>/<namespace>/<repo>/edit/<path:branchname>/"
  1842. "f/<path:filename>",
  1843. methods=["POST"],
  1844. )
  1845. @login_required
  1846. def fork_edit_file(repo, branchname, filename, username=None, namespace=None):
  1847. """Fork the project specified and open the specific file to edit"""
  1848. repo = flask.g.repo
  1849. form = pagure.forms.ConfirmationForm()
  1850. if not form.validate_on_submit():
  1851. flask.abort(400)
  1852. if pagure.lib.query._get_project(
  1853. flask.g.session,
  1854. repo.name,
  1855. namespace=repo.namespace,
  1856. user=flask.g.fas_user.username,
  1857. ):
  1858. flask.flash("You had already forked this project")
  1859. return flask.redirect(
  1860. flask.url_for(
  1861. "ui_ns.edit_file",
  1862. username=flask.g.fas_user.username,
  1863. namespace=namespace,
  1864. repo=repo.name,
  1865. branchname=branchname,
  1866. filename=filename,
  1867. )
  1868. )
  1869. try:
  1870. task = pagure.lib.query.fork_project(
  1871. session=flask.g.session,
  1872. repo=repo,
  1873. user=flask.g.fas_user.username,
  1874. editbranch=branchname,
  1875. editfile=filename,
  1876. )
  1877. flask.g.session.commit()
  1878. return pagure.utils.wait_for_task(task)
  1879. except pagure.exceptions.PagureException as err:
  1880. flask.flash(str(err), "error")
  1881. except SQLAlchemyError as err: # pragma: no cover
  1882. flask.g.session.rollback()
  1883. flask.flash(str(err), "error")
  1884. return flask.redirect(
  1885. flask.url_for(
  1886. "ui_ns.view_repo",
  1887. repo=repo.name,
  1888. username=username,
  1889. namespace=namespace,
  1890. )
  1891. )
  1892. _REACTION_URL_SNIPPET = (
  1893. "pull-request/<int:requestid>/comment/<int:commentid>/react"
  1894. )
  1895. @UI_NS.route("/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"])
  1896. @UI_NS.route("/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"])
  1897. @UI_NS.route(
  1898. "/<namespace>/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"]
  1899. )
  1900. @UI_NS.route(
  1901. "/<namespace>/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"]
  1902. )
  1903. @UI_NS.route(
  1904. "/fork/<username>/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"]
  1905. )
  1906. @UI_NS.route(
  1907. "/fork/<username>/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"]
  1908. )
  1909. @UI_NS.route(
  1910. "/fork/<username>/<namespace>/<repo>/%s/" % _REACTION_URL_SNIPPET,
  1911. methods=["POST"],
  1912. )
  1913. @UI_NS.route(
  1914. "/fork/<username>/<namespace>/<repo>/%s" % _REACTION_URL_SNIPPET,
  1915. methods=["POST"],
  1916. )
  1917. @login_required
  1918. def pull_request_comment_add_reaction(
  1919. repo, requestid, commentid, username=None, namespace=None
  1920. ):
  1921. repo = flask.g.repo
  1922. form = pagure.forms.ConfirmationForm()
  1923. if not form.validate_on_submit():
  1924. flask.abort(400, description="CSRF token not valid")
  1925. request = pagure.lib.query.search_pull_requests(
  1926. flask.g.session, requestid=requestid, project_id=repo.id
  1927. )
  1928. if not request:
  1929. flask.abort(404, description="Comment not found")
  1930. comment = pagure.lib.query.get_request_comment(
  1931. flask.g.session, request.uid, commentid
  1932. )
  1933. if "reaction" not in flask.request.form:
  1934. flask.abort(400, description="Reaction not found")
  1935. reactions = comment.reactions
  1936. r = flask.request.form["reaction"]
  1937. if not r:
  1938. flask.abort(400, description="Empty reaction is not acceptable")
  1939. if flask.g.fas_user.username in reactions.get(r, []):
  1940. flask.abort(409, description="Already posted this one")
  1941. reactions.setdefault(r, []).append(flask.g.fas_user.username)
  1942. comment.reactions = reactions
  1943. flask.g.session.add(comment)
  1944. try:
  1945. flask.g.session.commit()
  1946. except SQLAlchemyError as err: # pragma: no cover
  1947. flask.g.session.rollback()
  1948. _log.error(err)
  1949. return "error"
  1950. return "ok"