repo.py 115 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. Farhaan Bukhsh <farhaan.bukhsh@gmail.com>
  7. """
  8. # pylint: disable=too-many-lines
  9. # pylint: disable=too-many-branches
  10. # pylint: disable=too-many-locals
  11. # pylint: disable=too-many-statements
  12. # pylint: disable=bare-except
  13. # pylint: disable=broad-except
  14. from __future__ import unicode_literals, absolute_import
  15. import datetime
  16. import json
  17. import logging
  18. import os
  19. import re
  20. from math import ceil
  21. import flask
  22. import pygit2
  23. import kitchen.text.converters as ktc
  24. import six
  25. import werkzeug.utils
  26. from six import BytesIO
  27. from PIL import Image
  28. from sqlalchemy.exc import SQLAlchemyError
  29. from binaryornot.helpers import is_binary_string
  30. import pagure.exceptions
  31. import pagure.lib.git
  32. import pagure.lib.mimetype
  33. import pagure.lib.plugins
  34. import pagure.lib.query
  35. import pagure.lib.tasks
  36. import pagure.forms
  37. import pagure.ui.plugins
  38. from pagure.config import config as pagure_config
  39. from pagure.flask_app import _get_user
  40. from pagure.lib import encoding_utils
  41. from pagure.ui import UI_NS
  42. from pagure.utils import (
  43. __get_file_in_tree,
  44. login_required,
  45. is_true,
  46. stream_template,
  47. )
  48. from pagure.decorators import (
  49. is_repo_admin,
  50. is_admin_sess_timedout,
  51. has_issue_tracker,
  52. has_issue_or_pr_enabled,
  53. has_pr_enabled,
  54. )
  55. _log = logging.getLogger(__name__)
  56. def get_preferred_readme(tree):
  57. """ Establish some order about which README gets displayed
  58. if there are several in the repository. If none of the listed
  59. README files is availabe, display either the next file that
  60. starts with 'README' or nothing at all.
  61. """
  62. order = ["README.md", "README.rst", "README", "README.txt"]
  63. readmes = [x for x in tree if x.name.startswith("README")]
  64. if len(readmes) > 1:
  65. for i in order:
  66. for j in readmes:
  67. if i == j.name:
  68. return j
  69. elif len(readmes) == 1:
  70. return readmes[0]
  71. return None
  72. @UI_NS.route("/<repo>.git")
  73. @UI_NS.route("/<namespace>/<repo>.git")
  74. @UI_NS.route("/fork/<username>/<repo>.git")
  75. @UI_NS.route("/fork/<username>/<namespace>/<repo>.git")
  76. def view_repo_git(repo, username=None, namespace=None):
  77. """ Redirect to the project index page when user wants to view
  78. the git repo of the project
  79. """
  80. return flask.redirect(
  81. flask.url_for(
  82. "ui_ns.view_repo",
  83. repo=repo,
  84. username=username,
  85. namespace=namespace,
  86. )
  87. )
  88. @UI_NS.route("/<repo>/")
  89. @UI_NS.route("/<repo>")
  90. @UI_NS.route("/<namespace>/<repo>/")
  91. @UI_NS.route("/<namespace>/<repo>")
  92. @UI_NS.route("/fork/<username>/<repo>/")
  93. @UI_NS.route("/fork/<username>/<repo>")
  94. @UI_NS.route("/fork/<username>/<namespace>/<repo>/")
  95. @UI_NS.route("/fork/<username>/<namespace>/<repo>")
  96. def view_repo(repo, username=None, namespace=None):
  97. """ Front page of a specific repo.
  98. """
  99. repo_db = flask.g.repo
  100. repo_obj = flask.g.repo_obj
  101. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  102. head = repo_obj.head.shorthand
  103. else:
  104. head = None
  105. cnt = 0
  106. last_commits = []
  107. tree = []
  108. if not repo_obj.is_empty:
  109. try:
  110. for commit in repo_obj.walk(
  111. repo_obj.head.target, pygit2.GIT_SORT_NONE
  112. ):
  113. last_commits.append(commit)
  114. cnt += 1
  115. if cnt == 3:
  116. break
  117. tree = sorted(last_commits[0].tree, key=lambda x: x.filemode)
  118. except pygit2.GitError:
  119. pass
  120. readme = None
  121. safe = False
  122. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  123. branchname = repo_obj.head.shorthand
  124. else:
  125. branchname = None
  126. project = pagure.lib.query.get_authorized_project(
  127. flask.g.session, repo, user=username, namespace=namespace
  128. )
  129. watch_users = set()
  130. watch_users.add(project.user.username)
  131. for access_type in project.access_users.keys():
  132. for user in project.access_users[access_type]:
  133. watch_users.add(user.username)
  134. for watcher in project.watchers:
  135. if watcher.watch_issues or watcher.watch_commits:
  136. watch_users.add(watcher.user.username)
  137. readmefile = get_preferred_readme(tree)
  138. if readmefile:
  139. name, ext = os.path.splitext(readmefile.name)
  140. content = __get_file_in_tree(
  141. repo_obj, last_commits[0].tree, [readmefile.name]
  142. ).data
  143. readme, safe = pagure.doc_utils.convert_readme(
  144. content,
  145. ext,
  146. view_file_url=flask.url_for(
  147. "ui_ns.view_raw_file",
  148. username=username,
  149. repo=repo_db.name,
  150. identifier=branchname,
  151. filename="",
  152. ),
  153. )
  154. return flask.render_template(
  155. "repo_info.html",
  156. select="overview",
  157. repo=repo_db,
  158. username=username,
  159. head=head,
  160. readme=readme,
  161. safe=safe,
  162. origin="view_repo",
  163. branchname=branchname,
  164. last_commits=last_commits,
  165. tree=tree,
  166. num_watchers=len(watch_users),
  167. )
  168. """
  169. @UI_NS.route('/<repo>/branch/<path:branchname>')
  170. @UI_NS.route('/<namespace>/<repo>/branch/<path:branchname>')
  171. @UI_NS.route('/fork/<username>/<repo>/branch/<path:branchname>')
  172. @UI_NS.route('/fork/<username>/<namespace>/<repo>/branch/<path:branchname>')
  173. def view_repo_branch(repo, branchname, username=None, namespace=None):
  174. ''' Returns the list of branches in the repo. '''
  175. repo = flask.g.repo
  176. repo_obj = flask.g.repo_obj
  177. if branchname not in repo_obj.listall_branches():
  178. flask.abort(404, description='Branch not found')
  179. branch = repo_obj.lookup_branch(branchname)
  180. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  181. head = repo_obj.head.shorthand
  182. else:
  183. head = None
  184. cnt = 0
  185. last_commits = []
  186. for commit in repo_obj.walk(branch.peel().hex, pygit2.GIT_SORT_NONE):
  187. last_commits.append(commit)
  188. cnt += 1
  189. if cnt == 3:
  190. break
  191. diff_commits = []
  192. if repo.is_fork and repo.parent:
  193. parentname = repo.parent.repopath('main')
  194. else:
  195. parentname = repo.repopath('main')
  196. orig_repo = pygit2.Repository(parentname)
  197. tree = None
  198. safe = False
  199. readme = None
  200. if not repo_obj.is_empty and not orig_repo.is_empty:
  201. if not orig_repo.head_is_unborn:
  202. compare_branch = orig_repo.lookup_branch(orig_repo.head.shorthand)
  203. else:
  204. compare_branch = None
  205. commit_list = []
  206. if compare_branch:
  207. commit_list = [
  208. commit.oid.hex
  209. for commit in orig_repo.walk(
  210. compare_branch.peel().hex,
  211. pygit2.GIT_SORT_NONE)
  212. ]
  213. repo_commit = repo_obj[branch.peel().hex]
  214. for commit in repo_obj.walk(
  215. repo_commit.oid.hex, pygit2.GIT_SORT_NONE):
  216. if commit.oid.hex in commit_list:
  217. break
  218. diff_commits.append(commit.oid.hex)
  219. tree = sorted(last_commits[0].tree, key=lambda x: x.filemode)
  220. for i in tree:
  221. name, ext = os.path.splitext(i.name)
  222. if name == 'README':
  223. content = __get_file_in_tree(
  224. repo_obj, last_commits[0].tree, [i.name]).data
  225. readme, safe = pagure.doc_utils.convert_readme(
  226. content, ext,
  227. view_file_url=flask.url_for(
  228. 'ui_ns.view_raw_file', username=username,
  229. namespace=repo.namespace,
  230. repo=repo.name, identifier=branchname, filename=''))
  231. return flask.render_template(
  232. 'repo_info.html',
  233. select='overview',
  234. repo=repo,
  235. head=head,
  236. username=username,
  237. branchname=branchname,
  238. origin='view_repo_branch',
  239. last_commits=last_commits,
  240. tree=tree,
  241. safe=safe,
  242. readme=readme,
  243. diff_commits=diff_commits,
  244. )
  245. """
  246. @UI_NS.route("/<repo>/commits/")
  247. @UI_NS.route("/<repo>/commits")
  248. @UI_NS.route("/<repo>/commits/<path:branchname>")
  249. @UI_NS.route("/<namespace>/<repo>/commits/")
  250. @UI_NS.route("/<namespace>/<repo>/commits")
  251. @UI_NS.route("/<namespace>/<repo>/commits/<path:branchname>")
  252. @UI_NS.route("/fork/<username>/<repo>/commits/")
  253. @UI_NS.route("/fork/<username>/<repo>/commits")
  254. @UI_NS.route("/fork/<username>/<repo>/commits/<path:branchname>")
  255. @UI_NS.route("/fork/<username>/<namespace>/<repo>/commits/")
  256. @UI_NS.route("/fork/<username>/<namespace>/<repo>/commits")
  257. @UI_NS.route("/fork/<username>/<namespace>/<repo>/commits/<path:branchname>")
  258. def view_commits(repo, branchname=None, username=None, namespace=None):
  259. """ Displays the commits of the specified repo.
  260. """
  261. repo = flask.g.repo
  262. repo_obj = flask.g.repo_obj
  263. commit = None
  264. branch = None
  265. if branchname and branchname in repo_obj.listall_branches():
  266. branch = repo_obj.lookup_branch(branchname)
  267. commit = branch.peel(pygit2.Commit)
  268. elif branchname:
  269. try:
  270. commit = repo_obj.get(branchname)
  271. except (ValueError, TypeError):
  272. pass
  273. if "refs/tags/%s" % branchname in list(repo_obj.references):
  274. ref = repo_obj.lookup_reference("refs/tags/%s" % branchname)
  275. commit = ref.peel(pygit2.Commit)
  276. # If we're arriving here from the release page, we may have a Tag
  277. # where we expected a commit, in this case, get the actual commit
  278. if isinstance(commit, pygit2.Tag):
  279. commit = commit.peel(pygit2.Commit)
  280. branchname = commit.oid.hex
  281. elif isinstance(commit, pygit2.Blob):
  282. try:
  283. commit = commit.peel(pygit2.Commit)
  284. branchname = commit.oid.hex
  285. except Exception:
  286. flask.abort(
  287. 404, description="Invalid branch/identifier provided"
  288. )
  289. elif not repo_obj.is_empty and not repo_obj.head_is_unborn:
  290. branch = repo_obj.lookup_branch(repo_obj.head.shorthand)
  291. commit = branch.peel(pygit2.Commit)
  292. branchname = branch.branch_name
  293. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  294. head = repo_obj.head.shorthand
  295. else:
  296. head = None
  297. try:
  298. page = int(flask.request.args.get("page", 1))
  299. except (ValueError, TypeError):
  300. page = 1
  301. author = flask.request.args.get("author", None)
  302. author_obj = None
  303. if author:
  304. try:
  305. author_obj = pagure.lib.query.get_user(flask.g.session, author)
  306. except pagure.exceptions.PagureException:
  307. pass
  308. if not author_obj:
  309. flask.flash("No user found for the author: %s" % author, "error")
  310. limit = pagure_config["ITEM_PER_PAGE"]
  311. start = limit * (page - 1)
  312. end = limit * page
  313. n_commits = 0
  314. last_commits = []
  315. if commit:
  316. for commit in repo_obj.walk(commit.hex, pygit2.GIT_SORT_NONE):
  317. # Filters the commits for a user
  318. if author_obj:
  319. tmp = False
  320. for email in author_obj.emails:
  321. if email.email == commit.author.email:
  322. tmp = True
  323. break
  324. if not tmp:
  325. continue
  326. if n_commits >= start and n_commits <= end:
  327. last_commits.append(commit)
  328. n_commits += 1
  329. total_page = int(ceil(n_commits / float(limit)) if n_commits > 0 else 1)
  330. diff_commits = []
  331. diff_commits_full = []
  332. if repo.is_fork and repo.parent:
  333. parentname = repo.parent.repopath("main")
  334. else:
  335. parentname = repo.repopath("main")
  336. orig_repo = pygit2.Repository(parentname)
  337. if (
  338. not repo_obj.is_empty
  339. and not orig_repo.is_empty
  340. and len(repo_obj.listall_branches()) > 1
  341. ):
  342. if not orig_repo.head_is_unborn:
  343. compare_branch = orig_repo.lookup_branch(orig_repo.head.shorthand)
  344. else:
  345. compare_branch = None
  346. if compare_branch and branch:
  347. (
  348. diff,
  349. diff_commits_full,
  350. orig_commit,
  351. ) = pagure.lib.git.get_diff_info(
  352. repo_obj,
  353. orig_repo,
  354. branch.branch_name,
  355. compare_branch.branch_name,
  356. )
  357. for commit in diff_commits_full:
  358. diff_commits.append(commit.oid.hex)
  359. return flask.render_template(
  360. "commits.html",
  361. select="commits",
  362. origin="view_commits",
  363. repo=repo,
  364. username=username,
  365. head=head,
  366. branchname=branchname,
  367. last_commits=last_commits,
  368. diff_commits=diff_commits,
  369. diff_commits_full=diff_commits_full,
  370. number_of_commits=n_commits,
  371. page=page,
  372. total_page=total_page,
  373. flag_statuses_labels=json.dumps(pagure_config["FLAG_STATUSES_LABELS"]),
  374. )
  375. @UI_NS.route("/<repo>/c/<commit1>..<commit2>/")
  376. @UI_NS.route("/<repo>/c/<commit1>..<commit2>")
  377. @UI_NS.route("/<namespace>/<repo>/c/<commit1>..<commit2>/")
  378. @UI_NS.route("/<namespace>/<repo>/c/<commit1>..<commit2>")
  379. @UI_NS.route("/fork/<username>/<repo>/c/<commit1>..<commit2>/")
  380. @UI_NS.route("/fork/<username>/<repo>/c/<commit1>..<commit2>")
  381. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commit1>..<commit2>/")
  382. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commit1>..<commit2>")
  383. def compare_commits(repo, commit1, commit2, username=None, namespace=None):
  384. """ Compares two commits for specified repo
  385. """
  386. repo = flask.g.repo
  387. repo_obj = flask.g.repo_obj
  388. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  389. head = repo_obj.head.shorthand
  390. else:
  391. head = None
  392. # Check commit1 and commit2 existence
  393. commit1_obj = repo_obj.get(commit1)
  394. commit2_obj = repo_obj.get(commit2)
  395. if commit1_obj is None:
  396. flask.abort(404, description="First commit does not exist")
  397. if commit2_obj is None:
  398. flask.abort(404, description="Last commit does not exist")
  399. # Get commits diff data
  400. diff = repo_obj.diff(commit1, commit2)
  401. # Get commits list
  402. diff_commits = []
  403. order = pygit2.GIT_SORT_NONE
  404. first_commit = commit1
  405. last_commit = commit2
  406. commits = [
  407. commit.oid.hex[: len(first_commit)]
  408. for commit in repo_obj.walk(last_commit, pygit2.GIT_SORT_NONE)
  409. ]
  410. if first_commit not in commits:
  411. first_commit = commit2
  412. last_commit = commit1
  413. for commit in repo_obj.walk(last_commit, order):
  414. diff_commits.append(commit)
  415. if commit.oid.hex == first_commit or commit.oid.hex.startswith(
  416. first_commit
  417. ):
  418. break
  419. if first_commit == commit2:
  420. diff_commits.reverse()
  421. return flask.render_template(
  422. "repo_comparecommits.html",
  423. select="commits",
  424. origin="compare_commits",
  425. repo=repo,
  426. username=username,
  427. head=head,
  428. commit1=commit1,
  429. commit2=commit2,
  430. diff=diff,
  431. diff_commits=diff_commits,
  432. )
  433. @UI_NS.route("/<repo>/blob/<path:identifier>/f/<path:filename>")
  434. @UI_NS.route("/<namespace>/<repo>/blob/<path:identifier>/f/<path:filename>")
  435. @UI_NS.route(
  436. "/fork/<username>/<repo>/blob/<path:identifier>/f/<path:filename>"
  437. )
  438. @UI_NS.route(
  439. "/fork/<username>/<namespace>/<repo>/blob/<path:identifier>/f/"
  440. "<path:filename>"
  441. )
  442. def view_file(repo, identifier, filename, username=None, namespace=None):
  443. """ Displays the content of a file or a tree for the specified repo.
  444. """
  445. repo = flask.g.repo
  446. repo_obj = flask.g.repo_obj
  447. if repo_obj.is_empty:
  448. flask.abort(404, description="Empty repo cannot have a file")
  449. if identifier in repo_obj.listall_branches():
  450. branchname = identifier
  451. branch = repo_obj.lookup_branch(identifier)
  452. commit = branch.peel(pygit2.Commit)
  453. else:
  454. try:
  455. commit = repo_obj.get(identifier)
  456. branchname = identifier
  457. except ValueError:
  458. if "master" not in repo_obj.listall_branches():
  459. flask.abort(404, description="Branch not found")
  460. # If it's not a commit id then it's part of the filename
  461. commit = repo_obj[repo_obj.head.target]
  462. branchname = "master"
  463. if isinstance(commit, pygit2.Tag):
  464. commit = commit.peel(pygit2.Commit)
  465. tree = None
  466. if isinstance(commit, pygit2.Tree):
  467. tree = commit
  468. elif isinstance(commit, pygit2.Commit):
  469. tree = commit.tree
  470. if tree and commit and not isinstance(commit, pygit2.Blob):
  471. content = __get_file_in_tree(
  472. repo_obj, tree, filename.split("/"), bail_on_tree=True
  473. )
  474. if not content:
  475. flask.abort(404, description="File not found")
  476. content = repo_obj[content.oid]
  477. else:
  478. content = commit
  479. if not content:
  480. flask.abort(404, description="File not found")
  481. readme = None
  482. safe = False
  483. readme_ext = None
  484. headers = {}
  485. huge = False
  486. isbinary = False
  487. if "data" in dir(content):
  488. isbinary = is_binary_string(content.data)
  489. if isinstance(content, pygit2.Blob):
  490. rawtext = is_true(flask.request.args.get("text"))
  491. ext = filename[filename.rfind(".") :]
  492. if ext in (
  493. ".gif",
  494. ".png",
  495. ".bmp",
  496. ".tif",
  497. ".tiff",
  498. ".jpg",
  499. ".jpeg",
  500. ".ppm",
  501. ".pnm",
  502. ".pbm",
  503. ".pgm",
  504. ".webp",
  505. ".ico",
  506. ):
  507. try:
  508. Image.open(BytesIO(content.data))
  509. output_type = "image"
  510. except IOError as err:
  511. _log.debug("Failed to load image %s, error: %s", filename, err)
  512. output_type = "binary"
  513. elif ext in (".rst", ".mk", ".md", ".markdown") and not rawtext:
  514. content, safe = pagure.doc_utils.convert_readme(content.data, ext)
  515. output_type = "markup"
  516. elif "data" in dir(content) and not isbinary:
  517. file_content = None
  518. try:
  519. file_content = encoding_utils.decode(
  520. ktc.to_bytes(content.data)
  521. )
  522. except pagure.exceptions.PagureException:
  523. # We cannot decode the file, so let's pretend it's a binary
  524. # file and let the user download it instead of displaying
  525. # it.
  526. output_type = "binary"
  527. if file_content is not None:
  528. output_type = "file"
  529. content = encoding_utils.decode(content.data)
  530. else:
  531. output_type = "binary"
  532. elif not isbinary:
  533. output_type = "file"
  534. huge = True
  535. safe = False
  536. content = content.data.decode("utf-8")
  537. else:
  538. output_type = "binary"
  539. elif isinstance(content, pygit2.Commit):
  540. flask.abort(404, description="File not found")
  541. else:
  542. content = sorted(content, key=lambda x: x.filemode)
  543. for i in content:
  544. name, ext = os.path.splitext(i.name)
  545. if not isinstance(name, six.text_type):
  546. name = name.decode("utf-8")
  547. if name == "README":
  548. readme_file = __get_file_in_tree(
  549. repo_obj, content, [i.name]
  550. ).data
  551. readme, safe = pagure.doc_utils.convert_readme(
  552. readme_file, ext
  553. )
  554. readme_ext = ext
  555. output_type = "tree"
  556. if output_type == "binary":
  557. headers[str("Content-Disposition")] = "attachment"
  558. return flask.Response(
  559. flask.stream_with_context(
  560. stream_template(
  561. flask.current_app,
  562. "file.html",
  563. select="tree",
  564. repo=repo,
  565. origin="view_file",
  566. username=username,
  567. branchname=branchname,
  568. filename=filename,
  569. content=content,
  570. output_type=output_type,
  571. readme=readme,
  572. readme_ext=readme_ext,
  573. safe=safe,
  574. huge=huge,
  575. )
  576. ),
  577. 200,
  578. headers,
  579. )
  580. @UI_NS.route("/<repo>/raw/<path:identifier>")
  581. @UI_NS.route("/<namespace>/<repo>/raw/<path:identifier>")
  582. @UI_NS.route("/<repo>/raw/<path:identifier>/f/<path:filename>")
  583. @UI_NS.route("/<namespace>/<repo>/raw/<path:identifier>/f/<path:filename>")
  584. @UI_NS.route("/fork/<username>/<repo>/raw/<path:identifier>")
  585. @UI_NS.route("/fork/<username>/<namespace>/<repo>/raw/<path:identifier>")
  586. @UI_NS.route("/fork/<username>/<repo>/raw/<path:identifier>/f/<path:filename>")
  587. @UI_NS.route(
  588. "/fork/<username>/<namespace>/<repo>/raw/<path:identifier>/f/"
  589. "<path:filename>"
  590. )
  591. def view_raw_file(
  592. repo, identifier, filename=None, username=None, namespace=None
  593. ):
  594. """ Displays the raw content of a file of a commit for the specified repo.
  595. """
  596. repo_obj = flask.g.repo_obj
  597. if repo_obj.is_empty:
  598. flask.abort(404, description="Empty repo cannot have a file")
  599. if identifier in repo_obj.listall_branches():
  600. branch = repo_obj.lookup_branch(identifier)
  601. commit = branch.peel(pygit2.Commit)
  602. else:
  603. try:
  604. commit = repo_obj.get(identifier)
  605. except ValueError:
  606. if "master" not in repo_obj.listall_branches():
  607. flask.abort(404, description="Branch not found")
  608. # If it's not a commit id then it's part of the filename
  609. commit = repo_obj[repo_obj.head.target]
  610. if not commit:
  611. flask.abort(404, description="Commit %s not found" % (identifier))
  612. if isinstance(commit, pygit2.Tag):
  613. commit = commit.peel(pygit2.Commit)
  614. if filename:
  615. if isinstance(commit, pygit2.Blob):
  616. content = commit
  617. else:
  618. content = __get_file_in_tree(
  619. repo_obj, commit.tree, filename.split("/"), bail_on_tree=True
  620. )
  621. if not content or isinstance(content, pygit2.Tree):
  622. flask.abort(404, description="File not found")
  623. data = repo_obj[content.oid].data
  624. else:
  625. if commit.parents:
  626. # We need to take this not so nice road to ensure that the
  627. # identifier retrieved from the URL is actually valid
  628. try:
  629. parent = repo_obj.revparse_single("%s^" % identifier)
  630. diff = repo_obj.diff(parent, commit)
  631. except (KeyError, ValueError):
  632. flask.abort(404, description="Identifier not found")
  633. else:
  634. # First commit in the repo
  635. diff = commit.tree.diff_to_tree(swap=True)
  636. data = diff.patch
  637. if not data:
  638. flask.abort(404, description="No content found")
  639. return (data, 200, pagure.lib.mimetype.get_type_headers(filename, data))
  640. @UI_NS.route("/<repo>/blame/<path:filename>")
  641. @UI_NS.route("/<namespace>/<repo>/blame/<path:filename>")
  642. @UI_NS.route("/fork/<username>/<repo>/blame/<path:filename>")
  643. @UI_NS.route("/fork/<username>/<namespace>/<repo>/blame/<path:filename>")
  644. def view_blame_file(repo, filename, username=None, namespace=None):
  645. """ Displays the blame of a file or a tree for the specified repo.
  646. """
  647. repo = flask.g.repo
  648. repo_obj = flask.g.repo_obj
  649. branchname = flask.request.args.get("identifier")
  650. if repo_obj.is_empty:
  651. flask.abort(404, description="Empty repo cannot have a file")
  652. if branchname is None:
  653. if repo_obj.head_is_unborn:
  654. flask.abort(
  655. 404, description="Identifier is mandatory on unborn HEAD repos"
  656. )
  657. branchname = repo_obj.head.shorthand
  658. commit = repo_obj[repo_obj.head.target]
  659. else:
  660. if branchname in repo_obj.listall_branches():
  661. branch = repo_obj.lookup_branch(branchname)
  662. commit = branch.peel(pygit2.Commit)
  663. elif branchname in pagure.lib.git.get_git_tags(repo):
  664. branch = repo_obj.lookup_reference(
  665. "refs/tags/{}".format(branchname)
  666. )
  667. commit = branch.peel(pygit2.Commit)
  668. else:
  669. try:
  670. commit = repo_obj[branchname]
  671. except ValueError:
  672. flask.abort(
  673. 404, description="Cannot find specified identifier"
  674. )
  675. try:
  676. if isinstance(commit, pygit2.Tag):
  677. commit = commit.peel(pygit2.Commit)
  678. elif isinstance(commit, pygit2.Blob):
  679. commit = commit.peel(pygit2.Commit)
  680. except Exception:
  681. flask.abort(404, description="Invalid identified provided")
  682. content = __get_file_in_tree(
  683. repo_obj, commit.tree, filename.split("/"), bail_on_tree=True
  684. )
  685. if not content:
  686. flask.abort(404, description="File not found")
  687. if not isinstance(content, pygit2.Blob):
  688. flask.abort(404, description="File not found")
  689. if is_binary_string(content.data):
  690. flask.abort(400, description="Binary files cannot be blamed")
  691. try:
  692. content = encoding_utils.decode(content.data)
  693. except pagure.exceptions.PagureException:
  694. # We cannot decode the file, so bail but warn the admins
  695. _log.exception("File could not be decoded")
  696. flask.abort(500, description="File could not be decoded")
  697. blame = repo_obj.blame(filename, newest_commit=commit.oid.hex)
  698. return flask.render_template(
  699. "blame.html",
  700. select="tree",
  701. repo=repo,
  702. origin="view_file",
  703. username=username,
  704. filename=filename,
  705. branchname=branchname,
  706. content=content,
  707. output_type="blame",
  708. blame=blame,
  709. )
  710. @UI_NS.route("/<repo>/history/<path:filename>")
  711. @UI_NS.route("/<namespace>/<repo>/history/<path:filename>")
  712. @UI_NS.route("/fork/<username>/<repo>/history/<path:filename>")
  713. @UI_NS.route("/fork/<username>/<namespace>/<repo>/history/<path:filename>")
  714. def view_history_file(repo, filename, username=None, namespace=None):
  715. """ Displays the history of a file or a tree for the specified repo.
  716. """
  717. repo = flask.g.repo
  718. repo_obj = flask.g.repo_obj
  719. branchname = flask.request.args.get("identifier")
  720. if repo_obj.is_empty:
  721. flask.abort(404, description="Empty repo cannot have a file")
  722. if not branchname:
  723. try:
  724. branch = repo_obj.lookup_branch(repo_obj.head.shorthand)
  725. branchname = branch.branch_name
  726. except pygit2.GitError:
  727. flask.abort(400, description="Invalid repository")
  728. try:
  729. log = pagure.lib.repo.PagureRepo.log(
  730. flask.g.reponame,
  731. log_options=["--pretty=oneline", "--abbrev-commit"],
  732. target=filename,
  733. fromref=branchname,
  734. )
  735. if log.strip():
  736. log = [line.split(" ", 1) for line in log.strip().split("\n")]
  737. else:
  738. log = []
  739. except Exception:
  740. log = []
  741. if not log:
  742. flask.abort(400, description="No history could be found for this file")
  743. return flask.render_template(
  744. "file_history.html",
  745. select="tree",
  746. repo=repo,
  747. origin="view_file",
  748. username=username,
  749. filename=filename,
  750. branchname=branchname,
  751. output_type="history",
  752. log=log,
  753. )
  754. @UI_NS.route("/<repo>/c/<commitid>/")
  755. @UI_NS.route("/<repo>/c/<commitid>")
  756. @UI_NS.route("/<namespace>/<repo>/c/<commitid>/")
  757. @UI_NS.route("/<namespace>/<repo>/c/<commitid>")
  758. @UI_NS.route("/fork/<username>/<repo>/c/<commitid>/")
  759. @UI_NS.route("/fork/<username>/<repo>/c/<commitid>")
  760. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commitid>/")
  761. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commitid>")
  762. def view_commit(repo, commitid, username=None, namespace=None):
  763. """ Render a commit in a repo
  764. """
  765. repo = flask.g.repo
  766. if not repo:
  767. flask.abort(404, description="Project not found")
  768. repo_obj = flask.g.repo_obj
  769. branchname = flask.request.args.get("branch", None)
  770. splitview = flask.request.args.get("splitview", False)
  771. if "splitview" in flask.request.args:
  772. splitview = True
  773. else:
  774. splitview = False
  775. if branchname and branchname not in repo_obj.listall_branches():
  776. branchname = None
  777. try:
  778. commit = repo_obj.get(commitid)
  779. except ValueError:
  780. flask.abort(404, description="Commit not found")
  781. if commit is None:
  782. flask.abort(404, description="Commit not found")
  783. if isinstance(commit, pygit2.Blob):
  784. flask.abort(404, description="Commit not found")
  785. if isinstance(commit, pygit2.Tag):
  786. commit = commit.peel(pygit2.Commit)
  787. return flask.redirect(
  788. flask.url_for(
  789. "ui_ns.view_commit",
  790. repo=repo.name,
  791. username=username,
  792. namespace=repo.namespace,
  793. commitid=commit.hex,
  794. )
  795. )
  796. if commit.parents:
  797. diff = repo_obj.diff(commit.parents[0], commit)
  798. else:
  799. # First commit in the repo
  800. diff = commit.tree.diff_to_tree(swap=True)
  801. if diff:
  802. diff.find_similar()
  803. return flask.render_template(
  804. "commit.html",
  805. select="commits",
  806. repo=repo,
  807. branchname=branchname,
  808. username=username,
  809. commitid=commitid,
  810. commit=commit,
  811. diff=diff,
  812. splitview=splitview,
  813. flags=pagure.lib.query.get_commit_flag(
  814. flask.g.session, repo, commitid
  815. ),
  816. )
  817. @UI_NS.route("/<repo>/c/<commitid>.patch")
  818. @UI_NS.route("/<namespace>/<repo>/c/<commitid>.patch")
  819. @UI_NS.route("/fork/<username>/<repo>/c/<commitid>.patch")
  820. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commitid>.patch")
  821. def view_commit_patch(repo, commitid, username=None, namespace=None):
  822. """ Render a commit in a repo as patch
  823. """
  824. return view_commit_patch_or_diff(
  825. repo, commitid, username, namespace, diff=False
  826. )
  827. @UI_NS.route("/<repo>/c/<commitid>.diff")
  828. @UI_NS.route("/<namespace>/<repo>/c/<commitid>.diff")
  829. @UI_NS.route("/fork/<username>/<repo>/c/<commitid>.diff")
  830. @UI_NS.route("/fork/<username>/<namespace>/<repo>/c/<commitid>.diff")
  831. def view_commit_diff(repo, commitid, username=None, namespace=None):
  832. """ Render a commit in a repo as diff
  833. """
  834. is_js = is_true(flask.request.args.get("js"))
  835. return view_commit_patch_or_diff(
  836. repo, commitid, username, namespace, diff=True, is_js=is_js
  837. )
  838. def view_commit_patch_or_diff(
  839. repo, commitid, username=None, namespace=None, diff=False, is_js=False
  840. ):
  841. """ Renders a commit either as a patch or as a diff. """
  842. repo_obj = flask.g.repo_obj
  843. if is_js:
  844. errorresponse = flask.jsonify(
  845. {"code": "ERROR", "message": "Commit not found"}
  846. )
  847. errorresponse.status_code = 404
  848. try:
  849. commit = repo_obj.get(commitid)
  850. except ValueError:
  851. if is_js:
  852. return errorresponse
  853. else:
  854. flask.abort(404, description="Commit not found")
  855. if commit is None:
  856. if is_js:
  857. return errorresponse
  858. else:
  859. flask.abort(404, description="Commit not found")
  860. if is_js:
  861. patches = pagure.lib.git.commit_to_patch(
  862. repo_obj, commit, diff_view=True, find_similar=True, separated=True
  863. )
  864. diffs = {}
  865. for idx, patch in enumerate(patches):
  866. diffs[idx + 1] = patch
  867. return flask.jsonify(diffs)
  868. else:
  869. patch = pagure.lib.git.commit_to_patch(
  870. repo_obj, commit, diff_view=diff
  871. )
  872. return flask.Response(patch, content_type="text/plain;charset=UTF-8")
  873. @UI_NS.route("/<repo>/tree/")
  874. @UI_NS.route("/<repo>/tree")
  875. @UI_NS.route("/<namespace>/<repo>/tree/")
  876. @UI_NS.route("/<namespace>/<repo>/tree")
  877. @UI_NS.route("/<repo>/tree/<path:identifier>")
  878. @UI_NS.route("/<namespace>/<repo>/tree/<path:identifier>")
  879. @UI_NS.route("/fork/<username>/<repo>/tree/")
  880. @UI_NS.route("/fork/<username>/<repo>/tree")
  881. @UI_NS.route("/fork/<username>/<namespace>/<repo>/tree/")
  882. @UI_NS.route("/fork/<username>/<namespace>/<repo>/tree")
  883. @UI_NS.route("/fork/<username>/<repo>/tree/<path:identifier>")
  884. @UI_NS.route("/fork/<username>/<namespace>/<repo>/tree/<path:identifier>")
  885. def view_tree(repo, identifier=None, username=None, namespace=None):
  886. """ Render the tree of the repo
  887. """
  888. repo = flask.g.repo
  889. repo_obj = flask.g.repo_obj
  890. branchname = None
  891. content = None
  892. output_type = None
  893. commit = None
  894. readme = None
  895. safe = False
  896. readme_ext = None
  897. if not repo_obj.is_empty:
  898. if identifier in repo_obj.listall_branches():
  899. branchname = identifier
  900. branch = repo_obj.lookup_branch(identifier)
  901. commit = branch.peel(pygit2.Commit)
  902. else:
  903. try:
  904. commit = repo_obj.get(identifier)
  905. branchname = identifier
  906. except (ValueError, TypeError):
  907. # If it's not a commit id then it's part of the filename
  908. if not repo_obj.head_is_unborn:
  909. branchname = repo_obj.head.shorthand
  910. commit = repo_obj[repo_obj.head.target]
  911. if identifier:
  912. flask.flash(
  913. "'%s' not found in the git repository, going back "
  914. "to: %s" % (identifier, branchname),
  915. "error",
  916. )
  917. # If we're arriving here from the release page, we may have a Tag
  918. # where we expected a commit, in this case, get the actual commit
  919. if isinstance(commit, pygit2.Tag):
  920. commit = commit.peel(pygit2.Commit)
  921. branchname = commit.oid.hex
  922. if commit and not isinstance(commit, pygit2.Blob):
  923. content = sorted(commit.tree, key=lambda x: x.filemode)
  924. for i in commit.tree:
  925. name, ext = os.path.splitext(i.name)
  926. if name == "README":
  927. readme_file = __get_file_in_tree(
  928. repo_obj, commit.tree, [i.name]
  929. ).data
  930. readme, safe = pagure.doc_utils.convert_readme(
  931. readme_file, ext
  932. )
  933. readme_ext = ext
  934. output_type = "tree"
  935. return flask.render_template(
  936. "file.html",
  937. select="tree",
  938. origin="view_tree",
  939. repo=repo,
  940. username=username,
  941. branchname=branchname,
  942. filename="",
  943. content=content,
  944. output_type=output_type,
  945. readme=readme,
  946. readme_ext=readme_ext,
  947. safe=safe,
  948. )
  949. @UI_NS.route("/<repo>/releases/")
  950. @UI_NS.route("/<repo>/releases")
  951. @UI_NS.route("/<namespace>/<repo>/releases/")
  952. @UI_NS.route("/<namespace>/<repo>/releases")
  953. @UI_NS.route("/fork/<username>/<repo>/releases/")
  954. @UI_NS.route("/fork/<username>/<repo>/releases")
  955. @UI_NS.route("/fork/<username>/<namespace>/<repo>/releases/")
  956. @UI_NS.route("/fork/<username>/<namespace>/<repo>/releases")
  957. def view_tags(repo, username=None, namespace=None):
  958. """ Presents all the tags of the project.
  959. """
  960. repo = flask.g.repo
  961. tags = pagure.lib.git.get_git_tags_objects(repo)
  962. upload_folder_path = pagure_config["UPLOAD_FOLDER_PATH"] or ""
  963. pagure_checksum = os.path.exists(
  964. os.path.join(upload_folder_path, repo.fullname, "CHECKSUMS")
  965. )
  966. return flask.render_template(
  967. "releases.html",
  968. select="tags",
  969. username=username,
  970. repo=repo,
  971. tags=tags,
  972. pagure_checksum=pagure_checksum,
  973. )
  974. @UI_NS.route("/<repo>/branches/")
  975. @UI_NS.route("/<repo>/branches")
  976. @UI_NS.route("/<namespace>/<repo>/branches/")
  977. @UI_NS.route("/<namespace>/<repo>/branches")
  978. @UI_NS.route("/fork/<username>/<repo>/branches/")
  979. @UI_NS.route("/fork/<username>/<repo>/branches")
  980. @UI_NS.route("/fork/<username>/<namespace>/<repo>/branches/")
  981. @UI_NS.route("/fork/<username>/<namespace>/<repo>/branches")
  982. def view_branches(repo, username=None, namespace=None):
  983. """ Branches
  984. """
  985. repo_db = flask.g.repo
  986. repo_obj = flask.g.repo_obj
  987. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  988. head = repo_obj.head.shorthand
  989. else:
  990. head = None
  991. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  992. branchname = repo_obj.head.shorthand
  993. else:
  994. branchname = None
  995. return flask.render_template(
  996. "repo_branches.html",
  997. select="branches",
  998. repo=repo_db,
  999. username=username,
  1000. head=head,
  1001. origin="view_repo",
  1002. branchname=branchname,
  1003. )
  1004. @UI_NS.route("/<repo>/forks/")
  1005. @UI_NS.route("/<repo>/forks")
  1006. @UI_NS.route("/<namespace>/<repo>/forks/")
  1007. @UI_NS.route("/<namespace>/<repo>/forks")
  1008. @UI_NS.route("/fork/<username>/<repo>/forks/")
  1009. @UI_NS.route("/fork/<username>/<repo>/forks")
  1010. @UI_NS.route("/fork/<username>/<namespace>/<repo>/forks/")
  1011. @UI_NS.route("/fork/<username>/<namespace>/<repo>/forks")
  1012. def view_forks(repo, username=None, namespace=None):
  1013. """ Forks
  1014. """
  1015. return flask.render_template(
  1016. "repo_forks.html", select="forks", username=username, repo=flask.g.repo
  1017. )
  1018. @UI_NS.route("/<repo>/upload/", methods=("GET", "POST"))
  1019. @UI_NS.route("/<repo>/upload", methods=("GET", "POST"))
  1020. @UI_NS.route("/<namespace>/<repo>/upload/", methods=("GET", "POST"))
  1021. @UI_NS.route("/<namespace>/<repo>/upload", methods=("GET", "POST"))
  1022. @UI_NS.route("/fork/<username>/<repo>/upload/", methods=("GET", "POST"))
  1023. @UI_NS.route("/fork/<username>/<repo>/upload", methods=("GET", "POST"))
  1024. @UI_NS.route(
  1025. "/fork/<username>/<namespace>/<repo>/upload/", methods=("GET", "POST")
  1026. )
  1027. @UI_NS.route(
  1028. "/fork/<username>/<namespace>/<repo>/upload", methods=("GET", "POST")
  1029. )
  1030. @login_required
  1031. @is_repo_admin
  1032. def new_release(repo, username=None, namespace=None):
  1033. """ Upload a new release.
  1034. """
  1035. if not pagure_config.get("UPLOAD_FOLDER_PATH") or not pagure_config.get(
  1036. "UPLOAD_FOLDER_URL"
  1037. ):
  1038. flask.abort(404)
  1039. repo = flask.g.repo
  1040. form = pagure.forms.UploadFileForm()
  1041. if form.validate_on_submit():
  1042. filenames = []
  1043. error = False
  1044. for filestream in flask.request.files.getlist("filestream"):
  1045. filename = werkzeug.utils.secure_filename(filestream.filename)
  1046. filenames.append(filename)
  1047. try:
  1048. folder = os.path.join(
  1049. pagure_config["UPLOAD_FOLDER_PATH"], repo.fullname
  1050. )
  1051. if not os.path.exists(folder):
  1052. os.makedirs(folder)
  1053. dest = os.path.join(folder, filename)
  1054. if os.path.exists(dest):
  1055. raise pagure.exceptions.PagureException(
  1056. "This tarball has already been uploaded"
  1057. )
  1058. filestream.save(dest)
  1059. flask.flash('File "%s" uploaded' % filename)
  1060. except pagure.exceptions.PagureException as err:
  1061. _log.debug(err)
  1062. flask.flash(str(err), "error")
  1063. error = True
  1064. except Exception as err: # pragma: no cover
  1065. _log.exception(err)
  1066. flask.flash("Upload failed", "error")
  1067. error = True
  1068. if not error:
  1069. task = pagure.lib.tasks.update_checksums_file.delay(
  1070. folder=folder, filenames=filenames
  1071. )
  1072. _log.info(
  1073. "Updating checksums for %s of project %s in task: %s"
  1074. % (filenames, repo.fullname, task.id)
  1075. )
  1076. return flask.redirect(
  1077. flask.url_for(
  1078. "ui_ns.view_tags",
  1079. repo=repo.name,
  1080. username=username,
  1081. namespace=repo.namespace,
  1082. )
  1083. )
  1084. return flask.render_template(
  1085. "new_release.html",
  1086. select="tags",
  1087. username=username,
  1088. repo=repo,
  1089. form=form,
  1090. )
  1091. @UI_NS.route("/<repo>/settings/", methods=("GET", "POST"))
  1092. @UI_NS.route("/<repo>/settings", methods=("GET", "POST"))
  1093. @UI_NS.route("/<namespace>/<repo>/settings/", methods=("GET", "POST"))
  1094. @UI_NS.route("/<namespace>/<repo>/settings", methods=("GET", "POST"))
  1095. @UI_NS.route("/fork/<username>/<repo>/settings/", methods=("GET", "POST"))
  1096. @UI_NS.route("/fork/<username>/<repo>/settings", methods=("GET", "POST"))
  1097. @UI_NS.route(
  1098. "/fork/<username>/<namespace>/<repo>/settings/", methods=("GET", "POST")
  1099. )
  1100. @UI_NS.route(
  1101. "/fork/<username>/<namespace>/<repo>/settings", methods=("GET", "POST")
  1102. )
  1103. @login_required
  1104. @is_admin_sess_timedout
  1105. @is_repo_admin
  1106. def view_settings(repo, username=None, namespace=None):
  1107. """ Presents the settings of the project.
  1108. """
  1109. repo = flask.g.repo
  1110. repo_obj = flask.g.repo_obj
  1111. plugins = pagure.lib.plugins.get_plugin_names(
  1112. pagure_config.get("DISABLED_PLUGINS")
  1113. )
  1114. tags = pagure.lib.query.get_tags_of_project(flask.g.session, repo)
  1115. form = pagure.forms.ConfirmationForm()
  1116. tag_form = pagure.forms.AddIssueTagForm()
  1117. branches = repo_obj.listall_branches()
  1118. branches_form = pagure.forms.DefaultBranchForm(branches=branches)
  1119. priority_form = pagure.forms.DefaultPriorityForm(
  1120. priorities=repo.priorities.values()
  1121. )
  1122. if form.validate_on_submit():
  1123. settings = {}
  1124. for key in flask.request.form:
  1125. if key == "csrf_token":
  1126. continue
  1127. settings[key] = flask.request.form[key]
  1128. try:
  1129. message = pagure.lib.query.update_project_settings(
  1130. flask.g.session,
  1131. repo=repo,
  1132. settings=settings,
  1133. user=flask.g.fas_user.username,
  1134. )
  1135. flask.g.session.commit()
  1136. flask.flash(message)
  1137. return flask.redirect(
  1138. flask.url_for(
  1139. "ui_ns.view_repo",
  1140. username=username,
  1141. repo=repo.name,
  1142. namespace=repo.namespace,
  1143. )
  1144. )
  1145. except pagure.exceptions.PagureException as msg:
  1146. flask.g.session.rollback()
  1147. _log.debug(msg)
  1148. flask.flash(str(msg), "error")
  1149. except SQLAlchemyError as err: # pragma: no cover
  1150. flask.g.session.rollback()
  1151. flask.flash(str(err), "error")
  1152. if not repo_obj.is_empty and not repo_obj.head_is_unborn:
  1153. branchname = repo_obj.head.shorthand
  1154. else:
  1155. branchname = None
  1156. if flask.request.method == "GET" and branchname:
  1157. branches_form.branches.data = branchname
  1158. priority_form.priority.data = repo.default_priority
  1159. return flask.render_template(
  1160. "settings.html",
  1161. select="settings",
  1162. username=username,
  1163. repo=repo,
  1164. access_users=repo.access_users,
  1165. access_groups=repo.access_groups,
  1166. form=form,
  1167. tag_form=tag_form,
  1168. branches_form=branches_form,
  1169. priority_form=priority_form,
  1170. tags=tags,
  1171. plugins=plugins,
  1172. branchname=branchname,
  1173. pagure_admin=pagure.utils.is_admin(),
  1174. )
  1175. @UI_NS.route("/<repo>/settings/test_hook", methods=("GET", "POST"))
  1176. @UI_NS.route("/<namespace>/<repo>/settings/test_hook", methods=("GET", "POST"))
  1177. @UI_NS.route(
  1178. "/fork/<username>/<repo>/settings/test_hook", methods=("GET", "POST")
  1179. )
  1180. @UI_NS.route(
  1181. "/fork/<username>/<namespace>/<repo>/settings/test_hook",
  1182. methods=("GET", "POST"),
  1183. )
  1184. @login_required
  1185. @is_admin_sess_timedout
  1186. @is_repo_admin
  1187. def test_web_hook(repo, username=None, namespace=None):
  1188. """ Endpoint that can be called to send a test message to the web-hook
  1189. service allowing to test the web-hooks set.
  1190. """
  1191. repo = flask.g.repo
  1192. form = pagure.forms.ConfirmationForm()
  1193. if form.validate_on_submit():
  1194. pagure.lib.notify.log(
  1195. project=repo,
  1196. topic="Test.notification",
  1197. msg={"content": "Test message"},
  1198. webhook=True,
  1199. )
  1200. flask.flash("Notification triggered")
  1201. return flask.redirect(
  1202. flask.url_for(
  1203. "ui_ns.view_settings",
  1204. username=username,
  1205. repo=repo.name,
  1206. namespace=repo.namespace,
  1207. )
  1208. + "#projectoptions-tab"
  1209. )
  1210. @UI_NS.route("/<repo>/update", methods=["POST"])
  1211. @UI_NS.route("/<namespace>/<repo>/update", methods=["POST"])
  1212. @UI_NS.route("/fork/<username>/<repo>/update", methods=["POST"])
  1213. @UI_NS.route("/fork/<username>/<namespace>/<repo>/update", methods=["POST"])
  1214. @login_required
  1215. @is_admin_sess_timedout
  1216. @is_repo_admin
  1217. def update_project(repo, username=None, namespace=None):
  1218. """ Update the description of a project.
  1219. """
  1220. repo = flask.g.repo
  1221. form = pagure.forms.ProjectFormSimplified()
  1222. if form.validate_on_submit():
  1223. try:
  1224. repo.description = form.description.data
  1225. repo.avatar_email = form.avatar_email.data.strip()
  1226. repo.url = form.url.data.strip()
  1227. if repo.private:
  1228. repo.private = form.private.data
  1229. pagure.lib.query.update_tags(
  1230. flask.g.session,
  1231. repo,
  1232. tags=[t.strip() for t in form.tags.data.split(",")],
  1233. username=flask.g.fas_user.username,
  1234. )
  1235. flask.g.session.add(repo)
  1236. flask.g.session.commit()
  1237. flask.flash("Project updated")
  1238. except SQLAlchemyError as err: # pragma: no cover
  1239. flask.g.session.rollback()
  1240. flask.flash(str(err), "error")
  1241. else:
  1242. for field in form.errors:
  1243. flask.flash(
  1244. 'Field "%s" errored with errors: %s'
  1245. % (field, ", ".join(form.errors[field])),
  1246. "error",
  1247. )
  1248. return flask.redirect(
  1249. flask.url_for(
  1250. "ui_ns.view_settings",
  1251. username=username,
  1252. repo=repo.name,
  1253. namespace=repo.namespace,
  1254. )
  1255. + "#projectdetails-tab"
  1256. )
  1257. @UI_NS.route("/<repo>/update/priorities", methods=["POST"])
  1258. @UI_NS.route("/<namespace>/<repo>/update/priorities", methods=["POST"])
  1259. @UI_NS.route("/fork/<username>/<repo>/update/priorities", methods=["POST"])
  1260. @UI_NS.route(
  1261. "/fork/<username>/<namespace>/<repo>/update/priorities", methods=["POST"]
  1262. )
  1263. @login_required
  1264. @has_issue_tracker
  1265. @is_admin_sess_timedout
  1266. @is_repo_admin
  1267. def update_priorities(repo, username=None, namespace=None):
  1268. """ Update the priorities of a project.
  1269. """
  1270. repo = flask.g.repo
  1271. form = pagure.forms.ConfirmationForm()
  1272. error = False
  1273. if form.validate_on_submit():
  1274. weights = [
  1275. w.strip()
  1276. for w in flask.request.form.getlist("priority_weigth")
  1277. if w.strip()
  1278. ]
  1279. try:
  1280. weights = [int(w) for w in weights]
  1281. except (ValueError, TypeError):
  1282. flask.flash("Priorities weights must be numbers", "error")
  1283. error = True
  1284. titles = [
  1285. p.strip()
  1286. for p in flask.request.form.getlist("priority_title")
  1287. if p.strip()
  1288. ]
  1289. if len(weights) != len(titles):
  1290. flask.flash(
  1291. "Priorities weights and titles are not of the same length",
  1292. "error",
  1293. )
  1294. error = True
  1295. for weight in weights:
  1296. if weights.count(weight) != 1:
  1297. flask.flash(
  1298. "Priority weight %s is present %s times"
  1299. % (weight, weights.count(weight)),
  1300. "error",
  1301. )
  1302. error = True
  1303. break
  1304. for title in titles:
  1305. if titles.count(title) != 1:
  1306. flask.flash(
  1307. "Priority %s is present %s times"
  1308. % (title, titles.count(title)),
  1309. "error",
  1310. )
  1311. error = True
  1312. break
  1313. if not error:
  1314. priorities = {}
  1315. if weights:
  1316. for cnt in range(len(weights)):
  1317. priorities[weights[cnt]] = titles[cnt]
  1318. priorities[""] = ""
  1319. try:
  1320. repo.priorities = priorities
  1321. if repo.default_priority not in priorities.values():
  1322. flask.flash(
  1323. "Default priority reset as it is no longer one of "
  1324. "set priorities."
  1325. )
  1326. repo.default_priority = None
  1327. flask.g.session.add(repo)
  1328. flask.g.session.commit()
  1329. flask.flash("Priorities updated")
  1330. except SQLAlchemyError as err: # pragma: no cover
  1331. flask.g.session.rollback()
  1332. flask.flash(str(err), "error")
  1333. return flask.redirect(
  1334. flask.url_for(
  1335. "ui_ns.view_settings",
  1336. username=username,
  1337. repo=repo.name,
  1338. namespace=repo.namespace,
  1339. )
  1340. + "#priorities-tab"
  1341. )
  1342. @UI_NS.route("/<repo>/update/default_priority", methods=["POST"])
  1343. @UI_NS.route("/<namespace>/<repo>/update/default_priority", methods=["POST"])
  1344. @UI_NS.route(
  1345. "/fork/<username>/<repo>/update/default_priority", methods=["POST"]
  1346. )
  1347. @UI_NS.route(
  1348. "/fork/<username>/<namespace>/<repo>/update/default_priority",
  1349. methods=["POST"],
  1350. )
  1351. @login_required
  1352. @has_issue_tracker
  1353. @is_admin_sess_timedout
  1354. @is_repo_admin
  1355. def default_priority(repo, username=None, namespace=None):
  1356. """ Update the default priority of a project.
  1357. """
  1358. repo = flask.g.repo
  1359. form = pagure.forms.DefaultPriorityForm(
  1360. priorities=repo.priorities.values()
  1361. )
  1362. if form.validate_on_submit():
  1363. priority = form.priority.data or None
  1364. if priority in repo.priorities.values() or priority is None:
  1365. repo.default_priority = priority
  1366. try:
  1367. flask.g.session.add(repo)
  1368. flask.g.session.commit()
  1369. if priority:
  1370. flask.flash("Default priority set to %s" % priority)
  1371. else:
  1372. flask.flash("Default priority reset")
  1373. except SQLAlchemyError as err: # pragma: no cover
  1374. flask.g.session.rollback()
  1375. flask.flash(str(err), "error")
  1376. return flask.redirect(
  1377. flask.url_for(
  1378. "ui_ns.view_settings",
  1379. username=username,
  1380. repo=repo.name,
  1381. namespace=repo.namespace,
  1382. )
  1383. + "#priorities-tab"
  1384. )
  1385. @UI_NS.route("/<repo>/update/milestones", methods=["POST"])
  1386. @UI_NS.route("/<namespace>/<repo>/update/milestones", methods=["POST"])
  1387. @UI_NS.route("/fork/<username>/<repo>/update/milestones", methods=["POST"])
  1388. @UI_NS.route(
  1389. "/fork/<username>/<namespace>/<repo>/update/milestones", methods=["POST"]
  1390. )
  1391. @login_required
  1392. @has_issue_tracker
  1393. @is_admin_sess_timedout
  1394. @is_repo_admin
  1395. def update_milestones(repo, username=None, namespace=None):
  1396. """ Update the milestones of a project.
  1397. """
  1398. repo = flask.g.repo
  1399. form = pagure.forms.ConfirmationForm()
  1400. error = False
  1401. if form.validate_on_submit():
  1402. redirect = flask.request.args.get("from")
  1403. milestones = flask.request.form.getlist("milestones")
  1404. miles = {}
  1405. keys = []
  1406. for idx in milestones:
  1407. milestone = flask.request.form.get(
  1408. "milestone_%s_name" % (idx), None
  1409. )
  1410. date = flask.request.form.get("milestone_%s_date" % (idx), None)
  1411. active = (
  1412. True
  1413. if flask.request.form.get("milestone_%s_active" % (idx))
  1414. else False
  1415. )
  1416. if milestone and milestone.strip():
  1417. milestone = milestone.strip()
  1418. if milestone in miles:
  1419. flask.flash(
  1420. "Milestone %s is present multiple times" % milestone,
  1421. "error",
  1422. )
  1423. error = True
  1424. break
  1425. miles[milestone] = {
  1426. "date": date.strip() if date else None,
  1427. "active": active,
  1428. }
  1429. keys.append(milestone)
  1430. if not error:
  1431. try:
  1432. repo.milestones = miles
  1433. repo.milestones_keys = keys
  1434. flask.g.session.add(repo)
  1435. flask.g.session.commit()
  1436. flask.flash("Milestones updated")
  1437. except SQLAlchemyError as err: # pragma: no cover
  1438. flask.g.session.rollback()
  1439. flask.flash(str(err), "error")
  1440. if redirect == "issues":
  1441. return flask.redirect(
  1442. flask.url_for(
  1443. "ui_ns.view_issues",
  1444. username=username,
  1445. repo=repo.name,
  1446. namespace=namespace,
  1447. )
  1448. )
  1449. return flask.redirect(
  1450. flask.url_for(
  1451. "ui_ns.view_settings",
  1452. username=username,
  1453. repo=repo.name,
  1454. namespace=namespace,
  1455. )
  1456. + "#roadmap-tab"
  1457. )
  1458. @UI_NS.route("/<repo>/default/branch/", methods=["POST"])
  1459. @UI_NS.route("/<namespace>/<repo>/default/branch/", methods=["POST"])
  1460. @UI_NS.route("/fork/<username>/<repo>/default/branch/", methods=["POST"])
  1461. @UI_NS.route(
  1462. "/fork/<username>/<namespace>/<repo>/default/branch/", methods=["POST"]
  1463. )
  1464. @login_required
  1465. @is_admin_sess_timedout
  1466. @is_repo_admin
  1467. def change_ref_head(repo, username=None, namespace=None):
  1468. """ Change HEAD reference
  1469. """
  1470. repo = flask.g.repo
  1471. repo_obj = flask.g.repo_obj
  1472. branches = repo_obj.listall_branches()
  1473. form = pagure.forms.DefaultBranchForm(branches=branches)
  1474. if form.validate_on_submit():
  1475. branchname = form.branches.data
  1476. try:
  1477. pagure.lib.git.git_set_ref_head(project=repo, branch=branchname)
  1478. flask.flash("Default branch updated to %s" % branchname)
  1479. except Exception as err: # pragma: no cover
  1480. _log.exception(err)
  1481. return flask.redirect(
  1482. flask.url_for(
  1483. "ui_ns.view_settings",
  1484. username=username,
  1485. repo=repo.name,
  1486. namespace=namespace,
  1487. )
  1488. + "#defaultbranch-tab"
  1489. )
  1490. @UI_NS.route("/<repo>/delete", methods=["POST"])
  1491. @UI_NS.route("/<namespace>/<repo>/delete", methods=["POST"])
  1492. @UI_NS.route("/fork/<username>/<repo>/delete", methods=["POST"])
  1493. @UI_NS.route("/fork/<username>/<namespace>/<repo>/delete", methods=["POST"])
  1494. @login_required
  1495. @is_admin_sess_timedout
  1496. @is_repo_admin
  1497. def delete_repo(repo, username=None, namespace=None):
  1498. """ Delete the present project.
  1499. """
  1500. repo = flask.g.repo
  1501. del_project = pagure_config.get("ENABLE_DEL_PROJECTS", True)
  1502. del_fork = pagure_config.get("ENABLE_DEL_FORKS", del_project)
  1503. if (not repo.is_fork and not del_project) or (
  1504. repo.is_fork and not del_fork
  1505. ):
  1506. flask.abort(404)
  1507. if repo.read_only:
  1508. flask.flash(
  1509. "The ACLs of this project are being refreshed in the backend "
  1510. "this prevents the project from being deleted. Please wait "
  1511. "for this task to finish before trying again. Thanks!"
  1512. )
  1513. return flask.redirect(
  1514. flask.url_for(
  1515. "ui_ns.view_settings",
  1516. repo=repo.name,
  1517. username=username,
  1518. namespace=namespace,
  1519. )
  1520. + "#deleteproject-tab"
  1521. )
  1522. task = pagure.lib.tasks.delete_project.delay(
  1523. namespace=repo.namespace,
  1524. name=repo.name,
  1525. user=repo.user.user if repo.is_fork else None,
  1526. action_user=flask.g.fas_user.username,
  1527. )
  1528. return pagure.utils.wait_for_task(task)
  1529. @UI_NS.route("/<repo>/hook_token", methods=["POST"])
  1530. @UI_NS.route("/<namespace>/<repo>/hook_token", methods=["POST"])
  1531. @UI_NS.route("/fork/<username>/<repo>/hook_token", methods=["POST"])
  1532. @UI_NS.route(
  1533. "/fork/<username>/<namespace>/<repo>/hook_token", methods=["POST"]
  1534. )
  1535. @login_required
  1536. @is_admin_sess_timedout
  1537. @is_repo_admin
  1538. def new_repo_hook_token(repo, username=None, namespace=None):
  1539. """ Re-generate a hook token for the present project.
  1540. """
  1541. if not pagure_config.get("WEBHOOK", False):
  1542. flask.abort(404)
  1543. repo = flask.g.repo
  1544. form = pagure.forms.ConfirmationForm()
  1545. if not form.validate_on_submit():
  1546. flask.abort(400, description="Invalid request")
  1547. try:
  1548. repo.hook_token = pagure.lib.login.id_generator(40)
  1549. flask.g.session.commit()
  1550. flask.flash("New hook token generated")
  1551. except SQLAlchemyError as err: # pragma: no cover
  1552. flask.g.session.rollback()
  1553. _log.exception(err)
  1554. flask.flash("Could not generate a new token for this project", "error")
  1555. return flask.redirect(
  1556. flask.url_for(
  1557. "ui_ns.view_settings",
  1558. repo=repo.name,
  1559. username=username,
  1560. namespace=namespace,
  1561. )
  1562. + "#privatehookkey-tab"
  1563. )
  1564. @UI_NS.route("/<repo>/dropdeploykey/<int:keyid>", methods=["POST"])
  1565. @UI_NS.route("/<namespace>/<repo>/dropdeploykey/<int:keyid>", methods=["POST"])
  1566. @UI_NS.route(
  1567. "/fork/<username>/<repo>/dropdeploykey/<int:keyid>", methods=["POST"]
  1568. )
  1569. @UI_NS.route(
  1570. "/fork/<username>/<namespace>/<repo>/dropdeploykey/<int:keyid>",
  1571. methods=["POST"],
  1572. )
  1573. @login_required
  1574. @is_admin_sess_timedout
  1575. @is_repo_admin
  1576. def remove_deploykey(repo, keyid, username=None, namespace=None):
  1577. """ Remove the specified deploy key from the project.
  1578. """
  1579. if not pagure_config.get("DEPLOY_KEY", True):
  1580. flask.abort(
  1581. 404, description="This pagure instance disabled deploy keys"
  1582. )
  1583. repo = flask.g.repo
  1584. form = pagure.forms.ConfirmationForm()
  1585. if form.validate_on_submit():
  1586. found = False
  1587. sshkey = None
  1588. for key in repo.deploykeys:
  1589. if key.id == keyid:
  1590. sshkey = key.public_ssh_key
  1591. flask.g.session.delete(key)
  1592. found = True
  1593. break
  1594. if not found:
  1595. flask.flash("Deploy key does not exist in project.", "error")
  1596. return flask.redirect(
  1597. flask.url_for(
  1598. "ui_ns.view_settings",
  1599. repo=repo.name,
  1600. username=username,
  1601. namespace=repo.namespace,
  1602. )
  1603. + "#deploykeys-tab"
  1604. )
  1605. try:
  1606. flask.g.session.commit()
  1607. pagure.lib.query.create_deploykeys_ssh_keys_on_disk(
  1608. repo, pagure_config.get("GITOLITE_KEYDIR", None)
  1609. )
  1610. pagure.lib.tasks.gitolite_post_compile_only.delay()
  1611. if (
  1612. pagure_config.get("GIT_AUTH_BACKEND")
  1613. == "pagure_authorized_keys"
  1614. ):
  1615. _log.info("SSH FOLDER: %s", pagure_config.get("SSH_FOLDER"))
  1616. pagure.lib.tasks.remove_key_from_authorized_keys.delay(
  1617. ssh_folder=pagure_config.get("SSH_FOLDER"), sshkey=sshkey
  1618. )
  1619. flask.flash("Deploy key removed")
  1620. except SQLAlchemyError as err: # pragma: no cover
  1621. flask.g.session.rollback()
  1622. _log.exception(err)
  1623. flask.flash("Deploy key could not be removed", "error")
  1624. return flask.redirect(
  1625. flask.url_for(
  1626. "ui_ns.view_settings",
  1627. repo=repo.name,
  1628. username=username,
  1629. namespace=namespace,
  1630. )
  1631. + "#deploykey-tab"
  1632. )
  1633. @UI_NS.route("/<repo>/dropuser/<int:userid>", methods=["POST"])
  1634. @UI_NS.route("/<namespace>/<repo>/dropuser/<int:userid>", methods=["POST"])
  1635. @UI_NS.route("/fork/<username>/<repo>/dropuser/<int:userid>", methods=["POST"])
  1636. @UI_NS.route(
  1637. "/fork/<username>/<namespace>/<repo>/dropuser/<int:userid>",
  1638. methods=["POST"],
  1639. )
  1640. @login_required
  1641. @is_admin_sess_timedout
  1642. @is_repo_admin
  1643. def remove_user(repo, userid, username=None, namespace=None):
  1644. """ Remove the specified user from the project.
  1645. """
  1646. if not pagure_config.get("ENABLE_USER_MNGT", True):
  1647. flask.abort(
  1648. 404,
  1649. description="User management not allowed in the pagure instance",
  1650. )
  1651. repo = flask.g.repo
  1652. form = pagure.forms.ConfirmationForm()
  1653. delete_themselves = False
  1654. if form.validate_on_submit():
  1655. try:
  1656. user = pagure.lib.query.get_user_by_id(
  1657. flask.g.session, int(userid)
  1658. )
  1659. delete_themselves = user.username == flask.g.fas_user.username
  1660. msg = pagure.lib.query.remove_user_of_project(
  1661. flask.g.session, user, repo, flask.g.fas_user.username
  1662. )
  1663. flask.flash(msg)
  1664. except SQLAlchemyError as err: # pragma: no cover
  1665. flask.g.session.rollback()
  1666. _log.exception(err)
  1667. flask.flash("User could not be removed", "error")
  1668. except pagure.exceptions.PagureException as err:
  1669. flask.flash("%s" % err, "error")
  1670. return flask.redirect(
  1671. flask.url_for(
  1672. "ui_ns.view_settings",
  1673. repo=repo.name,
  1674. username=username,
  1675. namespace=repo.namespace,
  1676. )
  1677. + "#usersgroups-tab"
  1678. )
  1679. endpoint = "ui_ns.view_settings"
  1680. tab = "#usersgroups-tab"
  1681. if delete_themselves:
  1682. endpoint = "ui_ns.view_repo"
  1683. tab = ""
  1684. return flask.redirect(
  1685. flask.url_for(
  1686. endpoint, repo=repo.name, username=username, namespace=namespace
  1687. )
  1688. + tab
  1689. )
  1690. @UI_NS.route("/<repo>/adddeploykey/", methods=("GET", "POST"))
  1691. @UI_NS.route("/<repo>/adddeploykey", methods=("GET", "POST"))
  1692. @UI_NS.route("/<namespace>/<repo>/adddeploykey/", methods=("GET", "POST"))
  1693. @UI_NS.route("/<namespace>/<repo>/adddeploykey", methods=("GET", "POST"))
  1694. @UI_NS.route("/fork/<username>/<repo>/adddeploykey/", methods=("GET", "POST"))
  1695. @UI_NS.route("/fork/<username>/<repo>/adddeploykey", methods=("GET", "POST"))
  1696. @UI_NS.route(
  1697. "/fork/<username>/<namespace>/<repo>/adddeploykey/",
  1698. methods=("GET", "POST"),
  1699. )
  1700. @UI_NS.route(
  1701. "/fork/<username>/<namespace>/<repo>/adddeploykey", methods=("GET", "POST")
  1702. )
  1703. @login_required
  1704. @is_admin_sess_timedout
  1705. @is_repo_admin
  1706. def add_deploykey(repo, username=None, namespace=None):
  1707. """ Add the specified deploy key to the project.
  1708. """
  1709. if not pagure_config.get("DEPLOY_KEY", True):
  1710. flask.abort(
  1711. 404, description="This pagure instance disabled deploy keys"
  1712. )
  1713. repo = flask.g.repo
  1714. form = pagure.forms.AddDeployKeyForm()
  1715. if form.validate_on_submit():
  1716. user = _get_user(username=flask.g.fas_user.username)
  1717. try:
  1718. msg = pagure.lib.query.add_sshkey_to_project_or_user(
  1719. flask.g.session,
  1720. ssh_key=form.ssh_key.data,
  1721. creator=user,
  1722. project=repo,
  1723. pushaccess=form.pushaccess.data,
  1724. )
  1725. flask.g.session.commit()
  1726. pagure.lib.query.create_deploykeys_ssh_keys_on_disk(
  1727. repo, pagure_config.get("GITOLITE_KEYDIR", None)
  1728. )
  1729. pagure.lib.tasks.gitolite_post_compile_only.delay()
  1730. if (
  1731. pagure_config.get("GIT_AUTH_BACKEND")
  1732. == "pagure_authorized_keys"
  1733. ):
  1734. _log.info("SSH FOLDER: %s", pagure_config.get("SSH_FOLDER"))
  1735. pagure.lib.tasks.add_key_to_authorized_keys.delay(
  1736. ssh_folder=pagure_config.get("SSH_FOLDER"),
  1737. username=flask.g.fas_user.username,
  1738. sshkey=form.ssh_key.data,
  1739. )
  1740. flask.flash(msg)
  1741. return flask.redirect(
  1742. flask.url_for(
  1743. "ui_ns.view_settings",
  1744. repo=repo.name,
  1745. username=username,
  1746. namespace=namespace,
  1747. )
  1748. + "#deploykey-tab"
  1749. )
  1750. except pagure.exceptions.PagureException as msg:
  1751. flask.g.session.rollback()
  1752. _log.debug(msg)
  1753. flask.flash(str(msg), "error")
  1754. except SQLAlchemyError as err: # pragma: no cover
  1755. flask.g.session.rollback()
  1756. _log.exception(err)
  1757. flask.flash("Deploy key could not be added", "error")
  1758. return flask.render_template(
  1759. "add_deploykey.html", form=form, username=username, repo=repo
  1760. )
  1761. @UI_NS.route("/<repo>/adduser/", methods=("GET", "POST"))
  1762. @UI_NS.route("/<repo>/adduser", methods=("GET", "POST"))
  1763. @UI_NS.route("/<namespace>/<repo>/adduser/", methods=("GET", "POST"))
  1764. @UI_NS.route("/<namespace>/<repo>/adduser", methods=("GET", "POST"))
  1765. @UI_NS.route("/fork/<username>/<repo>/adduser/", methods=("GET", "POST"))
  1766. @UI_NS.route("/fork/<username>/<repo>/adduser", methods=("GET", "POST"))
  1767. @UI_NS.route(
  1768. "/fork/<username>/<namespace>/<repo>/adduser/", methods=("GET", "POST")
  1769. )
  1770. @UI_NS.route(
  1771. "/fork/<username>/<namespace>/<repo>/adduser", methods=("GET", "POST")
  1772. )
  1773. @login_required
  1774. @is_admin_sess_timedout
  1775. @is_repo_admin
  1776. def add_user(repo, username=None, namespace=None):
  1777. """ Add the specified user to the project.
  1778. """
  1779. if not pagure_config.get("ENABLE_USER_MNGT", True):
  1780. flask.abort(
  1781. 404,
  1782. description="User management is not allowed in this "
  1783. "pagure instance",
  1784. )
  1785. repo = flask.g.repo
  1786. user_to_update = flask.request.args.get("user", "").strip()
  1787. user_to_update_obj = None
  1788. user_access = None
  1789. if user_to_update:
  1790. user_to_update_obj = pagure.lib.query.search_user(
  1791. flask.g.session, username=user_to_update
  1792. )
  1793. user_access = pagure.lib.query.get_obj_access(
  1794. flask.g.session, repo, user_to_update_obj
  1795. )
  1796. # The requested user is not found
  1797. if user_to_update_obj is None:
  1798. user_to_update = None
  1799. user_access = None
  1800. form = pagure.forms.AddUserForm()
  1801. if form.validate_on_submit():
  1802. try:
  1803. msg = pagure.lib.query.add_user_to_project(
  1804. flask.g.session,
  1805. repo,
  1806. new_user=form.user.data,
  1807. user=flask.g.fas_user.username,
  1808. access=form.access.data,
  1809. required_groups=pagure_config.get("REQUIRED_GROUPS"),
  1810. )
  1811. flask.g.session.commit()
  1812. pagure.lib.git.generate_gitolite_acls(project=repo)
  1813. flask.flash(msg)
  1814. return flask.redirect(
  1815. flask.url_for(
  1816. "ui_ns.view_settings",
  1817. repo=repo.name,
  1818. username=username,
  1819. namespace=namespace,
  1820. )
  1821. + "#usersgroups-tab"
  1822. )
  1823. except pagure.exceptions.PagureException as msg:
  1824. flask.g.session.rollback()
  1825. _log.debug(msg)
  1826. flask.flash(str(msg), "error")
  1827. except SQLAlchemyError as err: # pragma: no cover
  1828. flask.g.session.rollback()
  1829. _log.exception(err)
  1830. flask.flash("User could not be added", "error")
  1831. access_levels = pagure.lib.query.get_access_levels(flask.g.session)
  1832. return flask.render_template(
  1833. "add_user.html",
  1834. form=form,
  1835. username=username,
  1836. repo=repo,
  1837. access_levels=access_levels,
  1838. user_to_update=user_to_update,
  1839. user_access=user_access,
  1840. )
  1841. @UI_NS.route("/<repo>/dropgroup/<int:groupid>", methods=["POST"])
  1842. @UI_NS.route("/<namespace>/<repo>/dropgroup/<int:groupid>", methods=["POST"])
  1843. @UI_NS.route(
  1844. "/fork/<username>/<repo>/dropgroup/<int:groupid>", methods=["POST"]
  1845. )
  1846. @UI_NS.route(
  1847. "/fork/<username>/<namespace>/<repo>/dropgroup/<int:groupid>",
  1848. methods=["POST"],
  1849. )
  1850. @login_required
  1851. @is_admin_sess_timedout
  1852. @is_repo_admin
  1853. def remove_group_project(repo, groupid, username=None, namespace=None):
  1854. """ Remove the specified group from the project.
  1855. """
  1856. if not pagure_config.get("ENABLE_USER_MNGT", True):
  1857. flask.abort(
  1858. 404,
  1859. description="User management is not allowed in this "
  1860. "pagure instance",
  1861. )
  1862. repo = flask.g.repo
  1863. form = pagure.forms.ConfirmationForm()
  1864. if form.validate_on_submit():
  1865. grpids = [grp.id for grp in repo.groups]
  1866. if groupid not in grpids:
  1867. flask.flash(
  1868. "Group does not seem to be part of this project", "error"
  1869. )
  1870. return flask.redirect(
  1871. flask.url_for(
  1872. "ui_ns.view_settings",
  1873. repo=repo.name,
  1874. username=username,
  1875. namespace=namespace,
  1876. )
  1877. + "#usersgroups-tab"
  1878. )
  1879. for grp in repo.groups:
  1880. if grp.id == groupid:
  1881. repo.groups.remove(grp)
  1882. break
  1883. try:
  1884. # Mark the project as read_only, celery will unmark it
  1885. pagure.lib.query.update_read_only_mode(
  1886. flask.g.session, repo, read_only=True
  1887. )
  1888. flask.g.session.commit()
  1889. pagure.lib.git.generate_gitolite_acls(project=repo)
  1890. flask.flash("Group removed")
  1891. except SQLAlchemyError as err: # pragma: no cover
  1892. flask.g.session.rollback()
  1893. _log.exception(err)
  1894. flask.flash("Group could not be removed", "error")
  1895. return flask.redirect(
  1896. flask.url_for(
  1897. "ui_ns.view_settings",
  1898. repo=repo.name,
  1899. username=username,
  1900. namespace=namespace,
  1901. )
  1902. + "#usersgroups-tab"
  1903. )
  1904. @UI_NS.route("/<repo>/addgroup/", methods=("GET", "POST"))
  1905. @UI_NS.route("/<repo>/addgroup", methods=("GET", "POST"))
  1906. @UI_NS.route("/<namespace>/<repo>/addgroup/", methods=("GET", "POST"))
  1907. @UI_NS.route("/<namespace>/<repo>/addgroup", methods=("GET", "POST"))
  1908. @UI_NS.route("/fork/<username>/<repo>/addgroup/", methods=("GET", "POST"))
  1909. @UI_NS.route("/fork/<username>/<repo>/addgroup", methods=("GET", "POST"))
  1910. @UI_NS.route(
  1911. "/fork/<username>/<namespace>/<repo>/addgroup/", methods=("GET", "POST")
  1912. )
  1913. @UI_NS.route(
  1914. "/fork/<username>/<namespace>/<repo>/addgroup", methods=("GET", "POST")
  1915. )
  1916. @login_required
  1917. @is_admin_sess_timedout
  1918. @is_repo_admin
  1919. def add_group_project(repo, username=None, namespace=None):
  1920. """ Add the specified group to the project.
  1921. """
  1922. if not pagure_config.get("ENABLE_USER_MNGT", True):
  1923. flask.abort(
  1924. 404,
  1925. description="User management is not allowed in this "
  1926. "pagure instance",
  1927. )
  1928. repo = flask.g.repo
  1929. group_to_update = flask.request.args.get("group", "").strip()
  1930. group_to_update_obj = None
  1931. group_access = None
  1932. if group_to_update:
  1933. group_to_update_obj = pagure.lib.query.search_groups(
  1934. flask.g.session, group_name=group_to_update
  1935. )
  1936. group_access = pagure.lib.query.get_obj_access(
  1937. flask.g.session, repo, group_to_update_obj
  1938. )
  1939. # The requested group is not found
  1940. if group_to_update_obj is None:
  1941. group_to_update = None
  1942. group_access = None
  1943. form = pagure.forms.AddGroupForm()
  1944. if form.validate_on_submit():
  1945. try:
  1946. msg = pagure.lib.query.add_group_to_project(
  1947. flask.g.session,
  1948. repo,
  1949. new_group=form.group.data,
  1950. user=flask.g.fas_user.username,
  1951. access=form.access.data,
  1952. create=pagure_config.get("ENABLE_GROUP_MNGT", False),
  1953. is_admin=pagure.utils.is_admin(),
  1954. )
  1955. flask.g.session.commit()
  1956. pagure.lib.git.generate_gitolite_acls(project=repo)
  1957. flask.flash(msg)
  1958. return flask.redirect(
  1959. flask.url_for(
  1960. "ui_ns.view_settings",
  1961. repo=repo.name,
  1962. username=username,
  1963. namespace=namespace,
  1964. )
  1965. + "#usersgroups-tab"
  1966. )
  1967. except pagure.exceptions.PagureException as msg:
  1968. flask.g.session.rollback()
  1969. _log.debug(msg)
  1970. flask.flash(str(msg), "error")
  1971. except SQLAlchemyError as err: # pragma: no cover
  1972. flask.g.session.rollback()
  1973. _log.exception(err)
  1974. flask.flash("Group could not be added", "error")
  1975. access_levels = pagure.lib.query.get_access_levels(flask.g.session)
  1976. return flask.render_template(
  1977. "add_group_project.html",
  1978. form=form,
  1979. username=username,
  1980. repo=repo,
  1981. access_levels=access_levels,
  1982. group_to_update=group_to_update,
  1983. group_access=group_access,
  1984. )
  1985. @UI_NS.route("/<repo>/regenerate", methods=["POST"])
  1986. @UI_NS.route("/<namespace>/<repo>/regenerate", methods=["POST"])
  1987. @UI_NS.route("/fork/<username>/<repo>/regenerate", methods=["POST"])
  1988. @UI_NS.route(
  1989. "/fork/<username>/<namespace>/<repo>/regenerate", methods=["POST"]
  1990. )
  1991. @login_required
  1992. @is_admin_sess_timedout
  1993. @is_repo_admin
  1994. def regenerate_git(repo, username=None, namespace=None):
  1995. """ Regenerate the specified git repo with the content in the project.
  1996. """
  1997. repo = flask.g.repo
  1998. regenerate = flask.request.form.get("regenerate")
  1999. if not regenerate or regenerate.lower() not in ["tickets", "requests"]:
  2000. flask.abort(
  2001. 400,
  2002. description="You can only regenerate tickest or requests repos",
  2003. )
  2004. form = pagure.forms.ConfirmationForm()
  2005. if form.validate_on_submit():
  2006. if regenerate.lower() == "requests" and repo.settings.get(
  2007. "pull_requests"
  2008. ):
  2009. # delete the requests repo and reinit
  2010. # in case there are no requests
  2011. if len(repo.requests) == 0:
  2012. pagure.lib.git.reinit_git(
  2013. project=repo, repofolder=pagure_config["REQUESTS_FOLDER"]
  2014. )
  2015. for request in repo.requests:
  2016. pagure.lib.git.update_git(request, repo=repo)
  2017. flask.flash("Requests git repo updating")
  2018. elif (
  2019. regenerate.lower() == "tickets"
  2020. and repo.settings.get("issue_tracker")
  2021. and pagure_config.get("ENABLE_TICKETS")
  2022. ):
  2023. # delete the ticket repo and reinit
  2024. # in case there are no tickets
  2025. if len(repo.issues) == 0:
  2026. pagure.lib.git.reinit_git(
  2027. project=repo, repofolder=pagure_config["TICKETS_FOLDER"]
  2028. )
  2029. for ticket in repo.issues:
  2030. pagure.lib.git.update_git(ticket, repo=repo)
  2031. flask.flash("Tickets git repo updating")
  2032. return flask.redirect(
  2033. flask.url_for(
  2034. "ui_ns.view_settings",
  2035. repo=repo.name,
  2036. username=username,
  2037. namespace=namespace,
  2038. )
  2039. + "#regen-tab"
  2040. )
  2041. @UI_NS.route("/<repo>/token/new/", methods=("GET", "POST"))
  2042. @UI_NS.route("/<repo>/token/new", methods=("GET", "POST"))
  2043. @UI_NS.route("/<namespace>/<repo>/token/new/", methods=("GET", "POST"))
  2044. @UI_NS.route("/<namespace>/<repo>/token/new", methods=("GET", "POST"))
  2045. @UI_NS.route("/fork/<username>/<repo>/token/new/", methods=("GET", "POST"))
  2046. @UI_NS.route("/fork/<username>/<repo>/token/new", methods=("GET", "POST"))
  2047. @UI_NS.route(
  2048. "/fork/<username>/<namespace>/<repo>/token/new/", methods=("GET", "POST")
  2049. )
  2050. @UI_NS.route(
  2051. "/fork/<username>/<namespace>/<repo>/token/new", methods=("GET", "POST")
  2052. )
  2053. @login_required
  2054. @is_admin_sess_timedout
  2055. def add_token(repo, username=None, namespace=None):
  2056. """ Add a token to a specified project.
  2057. """
  2058. repo = flask.g.repo
  2059. if not flask.g.repo_committer:
  2060. flask.abort(
  2061. 403,
  2062. description="You are not allowed to change the settings for "
  2063. "this project",
  2064. )
  2065. acls = pagure.lib.query.get_acls(
  2066. flask.g.session, restrict=pagure_config.get("USER_ACLS")
  2067. )
  2068. form = pagure.forms.NewTokenForm(acls=acls)
  2069. if form.validate_on_submit():
  2070. try:
  2071. pagure.lib.query.add_token_to_user(
  2072. flask.g.session,
  2073. repo,
  2074. description=form.description.data.strip() or None,
  2075. acls=form.acls.data,
  2076. username=flask.g.fas_user.username,
  2077. expiration_date=form.expiration_date.data,
  2078. )
  2079. flask.g.session.commit()
  2080. flask.flash("Token created")
  2081. return flask.redirect(
  2082. flask.url_for(
  2083. "ui_ns.view_settings",
  2084. repo=repo.name,
  2085. username=username,
  2086. namespace=namespace,
  2087. )
  2088. + "#apikeys-tab"
  2089. )
  2090. except SQLAlchemyError as err: # pragma: no cover
  2091. flask.g.session.rollback()
  2092. _log.exception(err)
  2093. flask.flash("API token could not be added", "error")
  2094. # When form is displayed after an empty submission, show an error.
  2095. if form.errors.get("acls"):
  2096. flask.flash("You must select at least one permission.", "error")
  2097. return flask.render_template(
  2098. "add_token.html",
  2099. select="settings",
  2100. form=form,
  2101. acls=acls,
  2102. username=username,
  2103. repo=repo,
  2104. )
  2105. @UI_NS.route("/<repo>/token/renew/<token_id>", methods=["POST"])
  2106. @UI_NS.route("/<namespace>/<repo>/token/renew/<token_id>", methods=["POST"])
  2107. @UI_NS.route(
  2108. "/fork/<username>/<repo>/token/renew/<token_id>", methods=["POST"]
  2109. )
  2110. @UI_NS.route(
  2111. "/fork/<username>/<namespace>/<repo>/token/renew/<token_id>",
  2112. methods=["POST"],
  2113. )
  2114. @login_required
  2115. @is_admin_sess_timedout
  2116. @is_repo_admin
  2117. def renew_api_token(repo, token_id, username=None, namespace=None):
  2118. """ Renew a token to a specified project.
  2119. """
  2120. repo = flask.g.repo
  2121. token = pagure.lib.query.get_api_token(flask.g.session, token_id)
  2122. if (
  2123. not token
  2124. or token.project.fullname != repo.fullname
  2125. or token.user.username != flask.g.fas_user.username
  2126. ):
  2127. flask.abort(404, description="Token not found")
  2128. form = pagure.forms.ConfirmationForm()
  2129. if form.validate_on_submit():
  2130. acls = [acl.name for acl in token.acls]
  2131. try:
  2132. pagure.lib.query.add_token_to_user(
  2133. flask.g.session,
  2134. repo,
  2135. description=token.description or None,
  2136. acls=acls,
  2137. username=flask.g.fas_user.username,
  2138. expiration_date=datetime.date.today()
  2139. + datetime.timedelta(days=(30 * 6)),
  2140. )
  2141. flask.g.session.commit()
  2142. flask.flash("Token created")
  2143. return flask.redirect(
  2144. flask.url_for(
  2145. "ui_ns.view_settings",
  2146. repo=repo.name,
  2147. username=username,
  2148. namespace=namespace,
  2149. )
  2150. + "#apikeys-tab"
  2151. )
  2152. except pagure.exceptions.PagureException as err:
  2153. flask.flash(str(err), "error")
  2154. except SQLAlchemyError as err: # pragma: no cover
  2155. flask.g.session.rollback()
  2156. _log.exception(err)
  2157. flask.flash("API token could not be renewed", "error")
  2158. return flask.redirect(
  2159. flask.url_for(
  2160. "ui_ns.view_settings",
  2161. repo=repo.name,
  2162. username=username,
  2163. namespace=namespace,
  2164. )
  2165. + "#apikeys-tab"
  2166. )
  2167. @UI_NS.route("/<repo>/token/revoke/<token_id>", methods=["POST"])
  2168. @UI_NS.route("/<namespace>/<repo>/token/revoke/<token_id>", methods=["POST"])
  2169. @UI_NS.route(
  2170. "/fork/<username>/<repo>/token/revoke/<token_id>", methods=["POST"]
  2171. )
  2172. @UI_NS.route(
  2173. "/fork/<username>/<namespace>/<repo>/token/revoke/<token_id>",
  2174. methods=["POST"],
  2175. )
  2176. @login_required
  2177. @is_admin_sess_timedout
  2178. @is_repo_admin
  2179. def revoke_api_token(repo, token_id, username=None, namespace=None):
  2180. """ Revokie a token to a specified project.
  2181. """
  2182. repo = flask.g.repo
  2183. token = pagure.lib.query.get_api_token(flask.g.session, token_id)
  2184. if (
  2185. not token
  2186. or token.project.fullname != repo.fullname
  2187. or token.user.username != flask.g.fas_user.username
  2188. ):
  2189. flask.abort(404, description="Token not found")
  2190. form = pagure.forms.ConfirmationForm()
  2191. if form.validate_on_submit():
  2192. try:
  2193. if token.expiration >= datetime.datetime.utcnow():
  2194. token.expiration = datetime.datetime.utcnow()
  2195. flask.g.session.add(token)
  2196. flask.g.session.commit()
  2197. flask.flash("Token revoked")
  2198. except SQLAlchemyError as err: # pragma: no cover
  2199. flask.g.session.rollback()
  2200. _log.exception(err)
  2201. message = flask.Markup(
  2202. "Token could not be revoked,"
  2203. ' please <a href="/about">contact an administrator</a>'
  2204. )
  2205. flask.flash(message, "error")
  2206. return flask.redirect(
  2207. flask.url_for(
  2208. "ui_ns.view_settings",
  2209. repo=repo.name,
  2210. username=username,
  2211. namespace=namespace,
  2212. )
  2213. + "#apikeys-tab"
  2214. )
  2215. @UI_NS.route(
  2216. "/<repo>/edit/<path:branchname>/f/<path:filename>", methods=("GET", "POST")
  2217. )
  2218. @UI_NS.route(
  2219. "/<namespace>/<repo>/edit/<path:branchname>/f/<path:filename>",
  2220. methods=("GET", "POST"),
  2221. )
  2222. @UI_NS.route(
  2223. "/fork/<username>/<repo>/edit/<path:branchname>/f/<path:filename>",
  2224. methods=("GET", "POST"),
  2225. )
  2226. @UI_NS.route(
  2227. "/fork/<username>/<namespace>/<repo>/edit/<path:branchname>/f/"
  2228. "<path:filename>",
  2229. methods=("GET", "POST"),
  2230. )
  2231. @login_required
  2232. @is_repo_admin
  2233. def edit_file(repo, branchname, filename, username=None, namespace=None):
  2234. """ Edit a file online.
  2235. """
  2236. repo = flask.g.repo
  2237. repo_obj = flask.g.repo_obj
  2238. user = pagure.lib.query.search_user(
  2239. flask.g.session, username=flask.g.fas_user.username
  2240. )
  2241. if repo_obj.is_empty:
  2242. flask.abort(404, description="Empty repo cannot have a file")
  2243. form = pagure.forms.EditFileForm(emails=user.emails)
  2244. branch = None
  2245. if branchname in repo_obj.listall_branches():
  2246. branch = repo_obj.lookup_branch(branchname)
  2247. commit = branch.peel(pygit2.Commit)
  2248. else:
  2249. flask.abort(400, description="Invalid branch specified")
  2250. if form.validate_on_submit():
  2251. try:
  2252. task = pagure.lib.tasks.update_file_in_git.delay(
  2253. repo.name,
  2254. repo.namespace,
  2255. repo.user.username if repo.is_fork else None,
  2256. branch=branchname,
  2257. branchto=form.branch.data,
  2258. filename=filename,
  2259. content=form.content.data,
  2260. message="%s\n\n%s"
  2261. % (
  2262. form.commit_title.data.strip(),
  2263. form.commit_message.data.strip(),
  2264. ),
  2265. username=user.username,
  2266. email=form.email.data,
  2267. )
  2268. return pagure.utils.wait_for_task(task)
  2269. except pagure.exceptions.PagureException as err: # pragma: no cover
  2270. _log.exception(err)
  2271. flask.flash("Commit could not be done", "error")
  2272. data = form.content.data
  2273. elif flask.request.method == "GET":
  2274. form.email.data = user.default_email
  2275. content = __get_file_in_tree(
  2276. repo_obj, commit.tree, filename.split("/")
  2277. )
  2278. if not content or isinstance(content, pygit2.Tree):
  2279. flask.abort(404, description="File not found")
  2280. if is_binary_string(content.data):
  2281. flask.abort(400, description="Cannot edit binary files")
  2282. try:
  2283. data = repo_obj[content.oid].data.decode("utf-8")
  2284. except UnicodeDecodeError: # pragma: no cover
  2285. # In theory we shouldn't reach here since we check if the file
  2286. # is binary with `is_binary_string()` above
  2287. flask.abort(400, description="Cannot edit binary files")
  2288. else:
  2289. data = form.content.data
  2290. if not isinstance(data, six.text_type):
  2291. data = data.decode("utf-8")
  2292. return flask.render_template(
  2293. "edit_file.html",
  2294. select="tree",
  2295. repo=repo,
  2296. username=username,
  2297. branchname=branchname,
  2298. data=data,
  2299. filename=filename,
  2300. form=form,
  2301. user=user,
  2302. )
  2303. @UI_NS.route("/<repo>/b/<path:branchname>/delete", methods=["POST"])
  2304. @UI_NS.route(
  2305. "/<namespace>/<repo>/b/<path:branchname>/delete", methods=["POST"]
  2306. )
  2307. @UI_NS.route(
  2308. "/fork/<username>/<repo>/b/<path:branchname>/delete", methods=["POST"]
  2309. )
  2310. @UI_NS.route(
  2311. "/fork/<username>/<namespace>/<repo>/b/<path:branchname>/delete",
  2312. methods=["POST"],
  2313. )
  2314. @login_required
  2315. def delete_branch(repo, branchname, username=None, namespace=None):
  2316. """ Delete the branch of a project.
  2317. """
  2318. if not flask.g.repo.is_fork and not pagure_config.get(
  2319. "ALLOW_DELETE_BRANCH", True
  2320. ):
  2321. flask.abort(
  2322. 404,
  2323. description="This pagure instance does not allow branch deletion",
  2324. )
  2325. repo_obj = flask.g.repo_obj
  2326. if not flask.g.repo_committer:
  2327. flask.abort(
  2328. 403,
  2329. description="You are not allowed to delete branch for "
  2330. "this project",
  2331. )
  2332. if six.PY2:
  2333. branchname = branchname.encode("utf-8")
  2334. if branchname == "master":
  2335. flask.abort(
  2336. 403, description="You are not allowed to delete the master branch"
  2337. )
  2338. if branchname not in repo_obj.listall_branches():
  2339. flask.abort(404, description="Branch not found")
  2340. task = pagure.lib.tasks.delete_branch.delay(
  2341. repo, namespace, username, branchname
  2342. )
  2343. return pagure.utils.wait_for_task(task)
  2344. @UI_NS.route("/docs/<repo>/")
  2345. @UI_NS.route("/docs/<repo>/<path:filename>")
  2346. @UI_NS.route("/docs/<namespace>/<repo>/")
  2347. @UI_NS.route("/docs/<namespace>/<repo>/<path:filename>")
  2348. @UI_NS.route("/docs/fork/<username>/<repo>/")
  2349. @UI_NS.route("/docs/fork/<username>/<namespace>/<repo>/<path:filename>")
  2350. @UI_NS.route("/docs/fork/<username>/<repo>/")
  2351. @UI_NS.route("/docs/fork/<username>/<namespace>/<repo>/<path:filename>")
  2352. def view_docs(repo, username=None, filename=None, namespace=None):
  2353. """ Display the documentation
  2354. """
  2355. repo = flask.g.repo
  2356. if not pagure_config.get("DOC_APP_URL"):
  2357. flask.abort(404, description="This pagure instance has no doc server")
  2358. return flask.render_template(
  2359. "docs.html",
  2360. select="docs",
  2361. repo=repo,
  2362. username=username,
  2363. filename=filename,
  2364. endpoint="view_docs",
  2365. )
  2366. @UI_NS.route("/<repo>/activity/")
  2367. @UI_NS.route("/<repo>/activity")
  2368. @UI_NS.route("/<namespace>/<repo>/activity/")
  2369. @UI_NS.route("/<namespace>/<repo>/activity")
  2370. def view_project_activity(repo, namespace=None):
  2371. """ Display the activity feed
  2372. """
  2373. if not pagure_config.get("DATAGREPPER_URL"):
  2374. flask.abort(404)
  2375. repo = flask.g.repo
  2376. return flask.render_template("activity.html", repo=repo)
  2377. @UI_NS.route("/<repo>/stargazers/")
  2378. @UI_NS.route("/fork/<username>/<repo>/stargazers/")
  2379. @UI_NS.route("/<namespace>/<repo>/stargazers/")
  2380. @UI_NS.route("/fork/<username>/<namespace>/<repo>/stargazers/")
  2381. def view_stargazers(repo, username=None, namespace=None):
  2382. """ View all the users who have starred the project """
  2383. stargazers = flask.g.repo.stargazers
  2384. users = [star.user for star in stargazers]
  2385. return flask.render_template(
  2386. "repo_stargazers.html",
  2387. repo=flask.g.repo,
  2388. username=username,
  2389. namespace=namespace,
  2390. users=users,
  2391. )
  2392. @UI_NS.route("/<repo>/star/<star>", methods=["POST"])
  2393. @UI_NS.route("/fork/<username>/<repo>/star/<star>", methods=["POST"])
  2394. @UI_NS.route("/<namespace>/<repo>/star/<star>", methods=["POST"])
  2395. @UI_NS.route(
  2396. "/fork/<username>/<namespace>/<repo>/star/<star>", methods=["POST"]
  2397. )
  2398. @login_required
  2399. def star_project(repo, star, username=None, namespace=None):
  2400. """ Star or Unstar a project
  2401. :arg repo: string representing the project which has to be starred or
  2402. unstarred.
  2403. :arg star: either '0' or '1' for unstar and star respectively
  2404. :arg username: string representing the user the fork of whose is being
  2405. starred or unstarred.
  2406. :arg namespace: namespace of the project if any
  2407. """
  2408. return_point = flask.url_for("ui_ns.index")
  2409. if flask.request.referrer is not None and pagure.utils.is_safe_url(
  2410. flask.request.referrer
  2411. ):
  2412. return_point = flask.request.referrer
  2413. form = pagure.forms.ConfirmationForm()
  2414. if not form.validate_on_submit():
  2415. flask.abort(400)
  2416. if star not in ["0", "1"]:
  2417. flask.abort(400)
  2418. try:
  2419. msg = pagure.lib.query.update_star_project(
  2420. flask.g.session,
  2421. user=flask.g.fas_user.username,
  2422. repo=flask.g.repo,
  2423. star=star,
  2424. )
  2425. flask.g.session.commit()
  2426. flask.flash(msg)
  2427. except SQLAlchemyError:
  2428. flask.flash("Could not star the project")
  2429. return flask.redirect(return_point)
  2430. @UI_NS.route("/<repo>/watch/settings/<watch>", methods=["POST"])
  2431. @UI_NS.route("/<namespace>/<repo>/watch/settings/<watch>", methods=["POST"])
  2432. @UI_NS.route(
  2433. "/fork/<username>/<repo>/watch/settings/<watch>", methods=["POST"]
  2434. )
  2435. @UI_NS.route(
  2436. "/fork/<username>/<namespace>/<repo>/watch/settings/<watch>",
  2437. methods=["POST"],
  2438. )
  2439. @login_required
  2440. def watch_repo(repo, watch, username=None, namespace=None):
  2441. """ Marked for watching or unwatching
  2442. """
  2443. return_point = flask.url_for("ui_ns.index")
  2444. if pagure.utils.is_safe_url(flask.request.referrer):
  2445. return_point = flask.request.referrer
  2446. form = pagure.forms.ConfirmationForm()
  2447. if not form.validate_on_submit():
  2448. flask.abort(400)
  2449. if "%s" % watch not in ["0", "1", "2", "3", "-1"]:
  2450. flask.abort(400)
  2451. try:
  2452. msg = pagure.lib.query.update_watch_status(
  2453. flask.g.session, flask.g.repo, flask.g.fas_user.username, watch
  2454. )
  2455. flask.g.session.commit()
  2456. flask.flash(msg)
  2457. except pagure.exceptions.PagureException as msg:
  2458. _log.debug(msg)
  2459. flask.flash(str(msg), "error")
  2460. return flask.redirect(return_point)
  2461. @UI_NS.route("/<repo>/update/public_notif", methods=["POST"])
  2462. @UI_NS.route("/<namespace>/<repo>/public_notif", methods=["POST"])
  2463. @UI_NS.route("/fork/<username>/<repo>/public_notif", methods=["POST"])
  2464. @UI_NS.route(
  2465. "/fork/<username>/<namespace>/<repo>/public_notif", methods=["POST"]
  2466. )
  2467. @login_required
  2468. @is_admin_sess_timedout
  2469. @is_repo_admin
  2470. def update_public_notifications(repo, username=None, namespace=None):
  2471. """ Update the public notification settings of a project.
  2472. """
  2473. repo = flask.g.repo
  2474. form = pagure.forms.PublicNotificationForm()
  2475. if form.validate_on_submit():
  2476. issue_notifs = [
  2477. w.strip() for w in form.issue_notifs.data.split(",") if w.strip()
  2478. ]
  2479. pr_notifs = [
  2480. w.strip() for w in form.pr_notifs.data.split(",") if w.strip()
  2481. ]
  2482. try:
  2483. notifs = repo.notifications
  2484. notifs["issues"] = issue_notifs
  2485. notifs["requests"] = pr_notifs
  2486. repo.notifications = notifs
  2487. flask.g.session.add(repo)
  2488. flask.g.session.commit()
  2489. flask.flash("Project updated")
  2490. except SQLAlchemyError as err: # pragma: no cover
  2491. flask.g.session.rollback()
  2492. flask.flash(str(err), "error")
  2493. else:
  2494. flask.flash(
  2495. "Unable to adjust one or more of the email provided", "error"
  2496. )
  2497. return flask.redirect(
  2498. flask.url_for(
  2499. "ui_ns.view_settings",
  2500. username=username,
  2501. repo=repo.name,
  2502. namespace=repo.namespace,
  2503. )
  2504. + "#publicnotifications-tab"
  2505. )
  2506. @UI_NS.route("/<repo>/update/close_status", methods=["POST"])
  2507. @UI_NS.route("/<namespace>/<repo>/update/close_status", methods=["POST"])
  2508. @UI_NS.route("/fork/<username>/<repo>/update/close_status", methods=["POST"])
  2509. @UI_NS.route(
  2510. "/fork/<username>/<namespace>/<repo>/update/close_status", methods=["POST"]
  2511. )
  2512. @login_required
  2513. @has_issue_tracker
  2514. @is_admin_sess_timedout
  2515. @is_repo_admin
  2516. def update_close_status(repo, username=None, namespace=None):
  2517. """ Update the close_status of a project.
  2518. """
  2519. repo = flask.g.repo
  2520. form = pagure.forms.ConfirmationForm()
  2521. if form.validate_on_submit():
  2522. close_status = [
  2523. w.strip()
  2524. for w in flask.request.form.getlist("close_status")
  2525. if w.strip()
  2526. ]
  2527. try:
  2528. repo.close_status = close_status
  2529. flask.g.session.add(repo)
  2530. flask.g.session.commit()
  2531. flask.flash("List of close status updated")
  2532. except SQLAlchemyError as err: # pragma: no cover
  2533. flask.g.session.rollback()
  2534. flask.flash(str(err), "error")
  2535. return flask.redirect(
  2536. flask.url_for(
  2537. "ui_ns.view_settings",
  2538. username=username,
  2539. repo=repo.name,
  2540. namespace=namespace,
  2541. )
  2542. + "#closestatus-tab"
  2543. )
  2544. @UI_NS.route("/<repo>/update/quick_replies", methods=["POST"])
  2545. @UI_NS.route("/<namespace>/<repo>/update/quick_replies", methods=["POST"])
  2546. @UI_NS.route("/fork/<username>/<repo>/update/quick_replies", methods=["POST"])
  2547. @UI_NS.route(
  2548. "/fork/<username>/<namespace>/<repo>/update/quick_replies",
  2549. methods=["POST"],
  2550. )
  2551. @login_required
  2552. @has_issue_tracker
  2553. @has_pr_enabled
  2554. @is_admin_sess_timedout
  2555. @is_repo_admin
  2556. def update_quick_replies(repo, username=None, namespace=None):
  2557. """ Update the quick_replies of a project.
  2558. """
  2559. repo = flask.g.repo
  2560. if not repo.settings.get("pull_requests", True):
  2561. flask.abort(
  2562. 404, description="Pull requests are disabled for this project"
  2563. )
  2564. form = pagure.forms.ConfirmationForm()
  2565. if form.validate_on_submit():
  2566. quick_replies = [
  2567. w.strip()
  2568. for w in flask.request.form.getlist("quick_reply")
  2569. if w.strip()
  2570. ]
  2571. try:
  2572. repo.quick_replies = quick_replies
  2573. flask.g.session.add(repo)
  2574. flask.g.session.commit()
  2575. flask.flash("List of quick replies updated")
  2576. except SQLAlchemyError as err: # pragma: no cover
  2577. flask.g.session.rollback()
  2578. flask.flash(str(err), "error")
  2579. return flask.redirect(
  2580. flask.url_for(
  2581. "ui_ns.view_settings",
  2582. username=username,
  2583. repo=repo.name,
  2584. namespace=namespace,
  2585. )
  2586. + "#quickreplies-tab"
  2587. )
  2588. @UI_NS.route("/<repo>/update/custom_keys", methods=["POST"])
  2589. @UI_NS.route("/<namespace>/<repo>/update/custom_keys", methods=["POST"])
  2590. @UI_NS.route("/fork/<username>/<repo>/update/custom_keys", methods=["POST"])
  2591. @UI_NS.route(
  2592. "/fork/<username>/<namespace>/<repo>/update/custom_keys", methods=["POST"]
  2593. )
  2594. @login_required
  2595. @has_issue_tracker
  2596. @is_admin_sess_timedout
  2597. @is_repo_admin
  2598. def update_custom_keys(repo, username=None, namespace=None):
  2599. """ Update the custom_keys of a project.
  2600. """
  2601. repo = flask.g.repo
  2602. form = pagure.forms.ConfirmationForm()
  2603. if form.validate_on_submit():
  2604. custom_keys = [
  2605. w.strip()
  2606. for w in flask.request.form.getlist("custom_keys")
  2607. if w.strip()
  2608. ]
  2609. custom_keys_type = [
  2610. w.strip()
  2611. for w in flask.request.form.getlist("custom_keys_type")
  2612. if w.strip()
  2613. ]
  2614. custom_keys_data = [
  2615. w.strip() for w in flask.request.form.getlist("custom_keys_data")
  2616. ]
  2617. custom_keys_notify = []
  2618. for idx in range(len(custom_keys)):
  2619. custom_keys_notify.append(
  2620. "%s"
  2621. % flask.request.form.get("custom_keys_notify-%s" % (idx + 1))
  2622. )
  2623. try:
  2624. msg = pagure.lib.query.set_custom_key_fields(
  2625. flask.g.session,
  2626. repo,
  2627. custom_keys,
  2628. custom_keys_type,
  2629. custom_keys_data,
  2630. custom_keys_notify,
  2631. )
  2632. flask.g.session.commit()
  2633. flask.flash(msg)
  2634. except SQLAlchemyError as err: # pragma: no cover
  2635. flask.g.session.rollback()
  2636. flask.flash(str(err), "error")
  2637. return flask.redirect(
  2638. flask.url_for(
  2639. "ui_ns.view_settings",
  2640. username=username,
  2641. repo=repo.name,
  2642. namespace=namespace,
  2643. )
  2644. + "#customfields-tab"
  2645. )
  2646. @UI_NS.route("/<repo>/delete/report", methods=["POST"])
  2647. @UI_NS.route("/<namespace>/<repo>/delete/report", methods=["POST"])
  2648. @UI_NS.route("/fork/<username>/<repo>/delete/report", methods=["POST"])
  2649. @UI_NS.route(
  2650. "/fork/<username>/<namespace>/<repo>/delete/report", methods=["POST"]
  2651. )
  2652. @login_required
  2653. @has_issue_tracker
  2654. @is_admin_sess_timedout
  2655. @is_repo_admin
  2656. def delete_report(repo, username=None, namespace=None):
  2657. """ Delete a report from a project.
  2658. """
  2659. repo = flask.g.repo
  2660. form = pagure.forms.ConfirmationForm()
  2661. if form.validate_on_submit():
  2662. report = flask.request.form.get("report")
  2663. reports = repo.reports
  2664. if report not in reports:
  2665. flask.flash("Unknown report: %s" % report, "error")
  2666. else:
  2667. del reports[report]
  2668. repo.reports = reports
  2669. try:
  2670. flask.g.session.add(repo)
  2671. flask.g.session.commit()
  2672. flask.flash("List of reports updated")
  2673. except SQLAlchemyError as err: # pragma: no cover
  2674. flask.g.session.rollback()
  2675. flask.flash(str(err), "error")
  2676. return flask.redirect(
  2677. flask.url_for(
  2678. "ui_ns.view_settings",
  2679. username=username,
  2680. repo=repo.name,
  2681. namespace=namespace,
  2682. )
  2683. + "#reports-tab"
  2684. )
  2685. @UI_NS.route("/<repo>/torepospanner", methods=["POST"])
  2686. @UI_NS.route("/<namespace>/<repo>/torepospanner", methods=["POST"])
  2687. @UI_NS.route("/fork/<username>/<repo>/torepospanner", methods=["POST"])
  2688. @UI_NS.route(
  2689. "/fork/<username>/<namespace>/<repo>/torepospanner", methods=["POST"]
  2690. )
  2691. @login_required
  2692. @is_admin_sess_timedout
  2693. @is_repo_admin
  2694. def move_to_repospanner(repo, username=None, namespace=None):
  2695. """ Give a project to someone else.
  2696. """
  2697. repo = flask.g.repo
  2698. if not pagure.utils.is_admin():
  2699. flask.abort(
  2700. 403,
  2701. description="You are not allowed to transfer this project "
  2702. "to repoSpanner",
  2703. )
  2704. if not pagure_config.get("REPOSPANNER_ADMIN_MIGRATION"):
  2705. flask.abort(
  2706. 403, description="It is not allowed to request migration of a repo"
  2707. )
  2708. form = pagure.forms.ConfirmationForm()
  2709. if form.validate_on_submit():
  2710. region = flask.request.form.get("region", "").strip()
  2711. if not region:
  2712. flask.abort(404, description="No target region specified")
  2713. if region not in pagure_config.get("REPOSPANNER_REGIONS"):
  2714. flask.abort(404, description="Invalid region specified")
  2715. _log.info(
  2716. "Repo %s requested to be migrated to repoSpanner region %s",
  2717. repo.fullname,
  2718. region,
  2719. )
  2720. task = pagure.lib.tasks.move_to_repospanner.delay(
  2721. repo.name, namespace, username, region
  2722. )
  2723. return pagure.utils.wait_for_task(
  2724. task,
  2725. prev=flask.url_for(
  2726. "ui_ns.view_repo",
  2727. username=username,
  2728. repo=repo.name,
  2729. namespace=namespace,
  2730. ),
  2731. )
  2732. return flask.redirect(
  2733. flask.url_for(
  2734. "ui_ns.view_repo",
  2735. username=username,
  2736. repo=repo.name,
  2737. namespace=namespace,
  2738. )
  2739. )
  2740. @UI_NS.route("/<repo>/give", methods=["POST"])
  2741. @UI_NS.route("/<namespace>/<repo>/give", methods=["POST"])
  2742. @UI_NS.route("/fork/<username>/<repo>/give", methods=["POST"])
  2743. @UI_NS.route("/fork/<username>/<namespace>/<repo>/give", methods=["POST"])
  2744. @login_required
  2745. @is_admin_sess_timedout
  2746. @is_repo_admin
  2747. def give_project(repo, username=None, namespace=None):
  2748. """ Give a project to someone else.
  2749. """
  2750. if not pagure_config.get("ENABLE_GIVE_PROJECTS", True):
  2751. flask.abort(404)
  2752. repo = flask.g.repo
  2753. if (
  2754. flask.g.fas_user.username != repo.user.user
  2755. and not pagure.utils.is_admin()
  2756. ):
  2757. flask.abort(
  2758. 403, description="You are not allowed to give this project"
  2759. )
  2760. form = pagure.forms.ConfirmationForm()
  2761. if form.validate_on_submit():
  2762. new_username = flask.request.form.get("user", "").strip()
  2763. if not new_username:
  2764. flask.abort(404, description="No user specified")
  2765. new_owner = pagure.lib.query.search_user(
  2766. flask.g.session, username=new_username
  2767. )
  2768. if not new_owner:
  2769. flask.abort(
  2770. 404, description="No such user %s found" % new_username
  2771. )
  2772. failed = False
  2773. try:
  2774. old_main_admin = repo.user.user
  2775. pagure.lib.query.set_project_owner(
  2776. flask.g.session,
  2777. repo,
  2778. new_owner,
  2779. required_groups=pagure_config.get("REQUIRED_GROUPS"),
  2780. )
  2781. flask.g.session.commit()
  2782. except pagure.exceptions.PagureException as msg:
  2783. failed = True
  2784. flask.g.session.rollback()
  2785. _log.debug(msg)
  2786. flask.flash(str(msg), "error")
  2787. except SQLAlchemyError: # pragma: no cover
  2788. failed = True
  2789. flask.g.session.rollback()
  2790. flask.flash(
  2791. "Due to a database error, this project could not be "
  2792. "transferred.",
  2793. "error",
  2794. )
  2795. if not failed:
  2796. try:
  2797. # If the person doing the action is the former main admin, keep
  2798. # them as admins
  2799. if flask.g.fas_user.username == old_main_admin:
  2800. pagure.lib.query.add_user_to_project(
  2801. flask.g.session,
  2802. repo,
  2803. new_user=flask.g.fas_user.username,
  2804. user=flask.g.fas_user.username,
  2805. )
  2806. flask.g.session.commit()
  2807. except pagure.exceptions.PagureException as msg:
  2808. flask.g.session.rollback()
  2809. _log.debug(msg)
  2810. except SQLAlchemyError: # pragma: no cover
  2811. flask.g.session.rollback()
  2812. flask.flash(
  2813. "Due to a database error, this access could not be "
  2814. "entirely set.",
  2815. "error",
  2816. )
  2817. pagure.lib.git.generate_gitolite_acls(project=repo)
  2818. flask.flash(
  2819. "The project has been transferred to %s" % new_username
  2820. )
  2821. return flask.redirect(
  2822. flask.url_for(
  2823. "ui_ns.view_repo",
  2824. username=username,
  2825. repo=repo.name,
  2826. namespace=namespace,
  2827. )
  2828. )
  2829. @UI_NS.route("/<repo>/dowait/")
  2830. @UI_NS.route("/<repo>/dowait")
  2831. @UI_NS.route("/<namespace>/<repo>/dowait/")
  2832. @UI_NS.route("/<namespace>/<repo>/dowait")
  2833. @UI_NS.route("/fork/<username>/<repo>/dowait/")
  2834. @UI_NS.route("/fork/<username>/<repo>/dowait")
  2835. @UI_NS.route("/fork/<username>/<namespace>/<repo>/dowait/")
  2836. @UI_NS.route("/fork/<username>/<namespace>/<repo>/dowait")
  2837. def project_dowait(repo, username=None, namespace=None):
  2838. """ Schedules a task that just waits 10 seconds for testing locking.
  2839. This is not available unless ALLOW_PROJECT_DOWAIT is set to True, which
  2840. should only ever be done in test instances.
  2841. """
  2842. if not pagure_config.get("ALLOW_PROJECT_DOWAIT", False):
  2843. flask.abort(401, description="No")
  2844. task = pagure.lib.tasks.project_dowait.delay(
  2845. name=repo, namespace=namespace, user=username
  2846. )
  2847. return pagure.utils.wait_for_task(task)
  2848. @UI_NS.route("/<repo>/stats/")
  2849. @UI_NS.route("/<repo>/stats")
  2850. @UI_NS.route("/<namespace>/<repo>/stats/")
  2851. @UI_NS.route("/<namespace>/<repo>/stats")
  2852. @UI_NS.route("/fork/<username>/<repo>/stats/")
  2853. @UI_NS.route("/fork/<username>/<repo>/stats")
  2854. @UI_NS.route("/fork/<username>/<namespace>/<repo>/stats/")
  2855. @UI_NS.route("/fork/<username>/<namespace>/<repo>/stats")
  2856. def view_stats(repo, username=None, namespace=None):
  2857. """ Displays some statistics about the specified repo.
  2858. """
  2859. return flask.render_template(
  2860. "repo_stats.html", select="stats", username=username, repo=flask.g.repo
  2861. )
  2862. @UI_NS.route("/<repo>/update/tags", methods=["POST"])
  2863. @UI_NS.route("/<namespace>/<repo>/update/tags", methods=["POST"])
  2864. @login_required
  2865. @is_repo_admin
  2866. @has_issue_or_pr_enabled
  2867. def update_tags(repo, username=None, namespace=None):
  2868. """ Update the tags of a project.
  2869. """
  2870. repo = flask.g.repo
  2871. form = pagure.forms.ConfirmationForm()
  2872. error = False
  2873. if form.validate_on_submit():
  2874. # Uniquify and order preserving
  2875. seen = set()
  2876. tags = [
  2877. tag.strip()
  2878. for tag in flask.request.form.getlist("tag")
  2879. if tag.strip()
  2880. and tag.strip() not in seen # noqa
  2881. and not seen.add(tag.strip())
  2882. ]
  2883. tag_descriptions = [
  2884. desc.strip()
  2885. for desc in flask.request.form.getlist("tag_description")
  2886. ]
  2887. # Uniquify and order preserving
  2888. colors = [
  2889. col.strip()
  2890. for col in flask.request.form.getlist("tag_color")
  2891. if col.strip()
  2892. ]
  2893. pattern = re.compile(pagure.forms.TAGS_REGEX, re.IGNORECASE)
  2894. for tag in tags:
  2895. if not pattern.match(tag):
  2896. flask.flash(
  2897. "Tag: %s contains one or more invalid characters" % tag,
  2898. "error",
  2899. )
  2900. error = True
  2901. color_pattern = re.compile(r"^#\w{3,6}$")
  2902. for color in colors:
  2903. if not color_pattern.match(color):
  2904. flask.flash(
  2905. "Color: %s does not match the expected pattern" % color,
  2906. "error",
  2907. )
  2908. error = True
  2909. if not (len(tags) == len(colors) == len(tag_descriptions)):
  2910. error = True
  2911. # Store the lengths because we are going to use them a lot
  2912. len_tags = len(tags)
  2913. len_tag_descriptions = len(tag_descriptions)
  2914. len_colors = len(colors)
  2915. error_message = "Error: Incomplete request. "
  2916. if len_colors > len_tags or len_tag_descriptions > len_tags:
  2917. error_message += "One or more tag fields missing."
  2918. elif len_colors < len_tags:
  2919. error_message += "One or more tag color fields missing."
  2920. elif len_tag_descriptions < len_tags:
  2921. error_message += "One or more tag description fields missing."
  2922. flask.flash(error_message, "error")
  2923. if not error:
  2924. known_tags = [tag.tag for tag in repo.tags_colored]
  2925. for idx, tag in enumerate(tags):
  2926. if tag in known_tags:
  2927. flask.flash("Duplicated tag: %s" % tag, "error")
  2928. break
  2929. try:
  2930. pagure.lib.query.new_tag(
  2931. flask.g.session,
  2932. tag,
  2933. tag_descriptions[idx],
  2934. colors[idx],
  2935. repo.id,
  2936. )
  2937. flask.g.session.commit()
  2938. flask.flash("Tags updated")
  2939. except SQLAlchemyError as err: # pragma: no cover
  2940. flask.g.session.rollback()
  2941. flask.flash(str(err), "error")
  2942. return flask.redirect(
  2943. flask.url_for(
  2944. "ui_ns.view_settings",
  2945. username=username,
  2946. repo=repo.name,
  2947. namespace=namespace,
  2948. )
  2949. + "#projecttags-tab"
  2950. )
  2951. @UI_NS.route("/<repo>/droptag/", methods=["POST"])
  2952. @UI_NS.route("/<namespace>/<repo>/droptag/", methods=["POST"])
  2953. @UI_NS.route("/fork/<username>/<repo>/droptag/", methods=["POST"])
  2954. @UI_NS.route("/fork/<username>/<namespace>/<repo>/droptag/", methods=["POST"])
  2955. @login_required
  2956. @is_repo_admin
  2957. @has_issue_or_pr_enabled
  2958. def remove_tag(repo, username=None, namespace=None):
  2959. """ Remove the specified tag, associated with the issues, from the project.
  2960. """
  2961. repo = flask.g.repo
  2962. form = pagure.forms.DeleteIssueTagForm()
  2963. if form.validate_on_submit():
  2964. tags = form.tag.data
  2965. tags = [tag.strip() for tag in tags.split(",")]
  2966. msgs = pagure.lib.query.remove_tags(
  2967. flask.g.session, repo, tags, user=flask.g.fas_user.username
  2968. )
  2969. try:
  2970. flask.g.session.commit()
  2971. for msg in msgs:
  2972. flask.flash(msg)
  2973. except SQLAlchemyError as err: # pragma: no cover
  2974. flask.g.session.rollback()
  2975. _log.error(err)
  2976. flask.flash("Could not remove tag: %s" % ",".join(tags), "error")
  2977. return flask.redirect(
  2978. flask.url_for(
  2979. "ui_ns.view_settings",
  2980. repo=repo.name,
  2981. username=username,
  2982. namespace=repo.namespace,
  2983. )
  2984. + "#projecttags-tab"
  2985. )
  2986. @UI_NS.route("/<repo>/tag/<tag>/edit/", methods=("GET", "POST"))
  2987. @UI_NS.route("/<repo>/tag/<tag>/edit", methods=("GET", "POST"))
  2988. @UI_NS.route("/<namespace>/<repo>/tag/<tag>/edit/", methods=("GET", "POST"))
  2989. @UI_NS.route("/<namespace>/<repo>/tag/<tag>/edit", methods=("GET", "POST"))
  2990. @UI_NS.route(
  2991. "/fork/<username>/<repo>/tag/<tag>/edit/", methods=("GET", "POST")
  2992. )
  2993. @UI_NS.route("/fork/<username>/<repo>/tag/<tag>/edit", methods=("GET", "POST"))
  2994. @UI_NS.route(
  2995. "/fork/<username>/<namespace>/<repo>/tag/<tag>/edit/",
  2996. methods=("GET", "POST"),
  2997. )
  2998. @UI_NS.route(
  2999. "/fork/<username>/<namespace>/<repo>/tag/<tag>/edit",
  3000. methods=("GET", "POST"),
  3001. )
  3002. @login_required
  3003. @is_repo_admin
  3004. @has_issue_or_pr_enabled
  3005. def edit_tag(repo, tag, username=None, namespace=None):
  3006. """ Edit the specified tag associated with the issues of a project.
  3007. """
  3008. repo = flask.g.repo
  3009. tags = pagure.lib.query.get_tags_of_project(flask.g.session, repo)
  3010. if not tags:
  3011. flask.abort(404, description="Project has no tags to edit")
  3012. # Check the tag exists, and get its old/original color
  3013. tagobj = pagure.lib.query.get_colored_tag(flask.g.session, tag, repo.id)
  3014. if not tagobj:
  3015. flask.abort(404, description="Tag %s not found in this project" % tag)
  3016. form = pagure.forms.AddIssueTagForm()
  3017. if form.validate_on_submit():
  3018. new_tag = form.tag.data
  3019. new_tag_description = form.tag_description.data
  3020. new_tag_color = form.tag_color.data
  3021. msgs = pagure.lib.query.edit_issue_tags(
  3022. flask.g.session,
  3023. repo,
  3024. tagobj,
  3025. new_tag,
  3026. new_tag_description,
  3027. new_tag_color,
  3028. user=flask.g.fas_user.username,
  3029. )
  3030. try:
  3031. flask.g.session.commit()
  3032. for msg in msgs:
  3033. flask.flash(msg)
  3034. except SQLAlchemyError as err: # pragma: no cover
  3035. flask.g.session.rollback()
  3036. _log.error(err)
  3037. flask.flash("Could not edit tag: %s" % tag, "error")
  3038. return flask.redirect(
  3039. flask.url_for(
  3040. "ui_ns.view_settings",
  3041. repo=repo.name,
  3042. username=username,
  3043. namespace=repo.namespace,
  3044. )
  3045. + "#projecttags-tab"
  3046. )
  3047. elif flask.request.method == "GET":
  3048. tag_color = tagobj.tag_color
  3049. if tag_color == "DeepSkyBlue":
  3050. tag_color = "#00bfff"
  3051. form.tag_color.data = tag_color
  3052. form.tag_description.data = tagobj.tag_description
  3053. form.tag.data = tag
  3054. return flask.render_template(
  3055. "edit_tag.html", username=username, repo=repo, form=form, tagname=tag
  3056. )
  3057. @UI_NS.route("/<repo>/archive/<ref>/<name>.tar")
  3058. @UI_NS.route("/<namespace>/<repo>/archive/<ref>/<name>.tar")
  3059. @UI_NS.route("/fork/<username>/<repo>/archive/<ref>/<name>.tar")
  3060. @UI_NS.route("/fork/<username>/<namespace>/<repo>/archive/<ref>/<name>.tar")
  3061. def get_project_archive_tar(repo, ref, name, namespace=None, username=None):
  3062. """ Generate an archive or redirect the user to where it already exists
  3063. """
  3064. return generate_project_archive(
  3065. repo,
  3066. ref,
  3067. name,
  3068. extension="tar",
  3069. namespace=namespace,
  3070. username=username,
  3071. )
  3072. @UI_NS.route("/<repo>/archive/<ref>/<name>.tar.gz")
  3073. @UI_NS.route("/<namespace>/<repo>/archive/<ref>/<name>.tar.gz")
  3074. @UI_NS.route("/fork/<username>/<repo>/archive/<ref>/<name>.tar.gz")
  3075. @UI_NS.route("/fork/<username>/<namespace>/<repo>/archive/<ref>/<name>.tar.gz")
  3076. def get_project_archive_tar_gz(repo, ref, name, namespace=None, username=None):
  3077. """ Generate an archive or redirect the user to where it already exists
  3078. """
  3079. return generate_project_archive(
  3080. repo,
  3081. ref,
  3082. name,
  3083. extension="tar.gz",
  3084. namespace=namespace,
  3085. username=username,
  3086. )
  3087. @UI_NS.route("/<repo>/archive/<ref>/<name>.zip")
  3088. @UI_NS.route("/<namespace>/<repo>/archive/<ref>/<name>.zip")
  3089. @UI_NS.route("/fork/<username>/<repo>/archive/<ref>/<name>.zip")
  3090. @UI_NS.route("/fork/<username>/<namespace>/<repo>/archive/<ref>/<name>.zip")
  3091. def get_project_archive_zip(repo, ref, name, namespace=None, username=None):
  3092. """ Generate an archive or redirect the user to where it already exists
  3093. """
  3094. return generate_project_archive(
  3095. repo,
  3096. ref,
  3097. name,
  3098. extension="zip",
  3099. namespace=namespace,
  3100. username=username,
  3101. )
  3102. def generate_project_archive(
  3103. repo, ref, name, extension, namespace=None, username=None
  3104. ):
  3105. """ Generate an archive or redirect the user to where it already
  3106. exists.
  3107. """
  3108. archive_folder = pagure_config.get("ARCHIVE_FOLDER")
  3109. if not archive_folder:
  3110. _log.debug("No ARCHIVE_FOLDER specified in the configuration")
  3111. flask.abort(
  3112. 404,
  3113. description="This pagure instance isn't configured to support "
  3114. "this feature",
  3115. )
  3116. if not os.path.exists(archive_folder):
  3117. _log.debug("No ARCHIVE_FOLDER could not be found on disk")
  3118. flask.abort(
  3119. 500,
  3120. description="Incorrect configuration, please contact your admin",
  3121. )
  3122. extensions = ["tar.gz", "tar", "zip"]
  3123. if extension not in extensions:
  3124. _log.debug("%s no in %s", extension, extensions)
  3125. flask.abort(400, description="Invalid archive format specified")
  3126. name = werkzeug.utils.secure_filename(name)
  3127. repo_obj = flask.g.repo_obj
  3128. ref_string = "refs/tags/%s" % ref
  3129. commit = None
  3130. tag = None
  3131. if ref_string in repo_obj.listall_references():
  3132. reference = repo_obj.lookup_reference(ref_string)
  3133. tag = repo_obj[reference.target]
  3134. if isinstance(tag, pygit2.Tag):
  3135. commit = tag.peel(pygit2.Commit)
  3136. elif isinstance(tag, pygit2.Commit):
  3137. commit = tag
  3138. else:
  3139. _log.debug("Found %s instead of a tag", tag)
  3140. flask.abort(400, description="Invalid reference provided")
  3141. else:
  3142. try:
  3143. commit = repo_obj.get(ref)
  3144. except ValueError:
  3145. flask.abort(404, description="Invalid commit provided")
  3146. if not isinstance(commit, pygit2.Commit):
  3147. flask.abort(400, description="Invalid reference specified")
  3148. tag_path = ""
  3149. tag_filename = None
  3150. if tag:
  3151. tag_filename = werkzeug.utils.secure_filename(ref)
  3152. tag_path = os.path.join("tags", tag_filename)
  3153. path = os.path.join(
  3154. archive_folder,
  3155. flask.g.repo.fullname,
  3156. tag_path,
  3157. commit.oid.hex,
  3158. "%s.%s" % (name, extension),
  3159. )
  3160. headers = {
  3161. str("Content-Disposition"): "attachment",
  3162. str("Content-Type"): "application/x-gzip",
  3163. }
  3164. if os.path.exists(path):
  3165. def _send_data():
  3166. with open(path, "rb") as stream:
  3167. yield stream.read()
  3168. _log.info("Sending the existing archive")
  3169. return flask.Response(
  3170. flask.stream_with_context(_send_data()), headers=headers
  3171. )
  3172. _log.info("Re-generating the archive")
  3173. task = pagure.lib.tasks.generate_archive.delay(
  3174. repo,
  3175. namespace=namespace,
  3176. username=username,
  3177. commit=commit.oid.hex,
  3178. tag=tag_filename,
  3179. name=name,
  3180. archive_fmt=extension,
  3181. )
  3182. def _wait_for_task_and_send_data():
  3183. while not task.ready():
  3184. import time
  3185. _log.info("waiting")
  3186. time.sleep(0.5)
  3187. with open(path, "rb") as stream:
  3188. yield stream.read()
  3189. _log.info("Sending the existing archive")
  3190. return flask.Response(
  3191. flask.stream_with_context(_wait_for_task_and_send_data()),
  3192. headers=headers,
  3193. )
  3194. return pagure.utils.wait_for_task(task)