tasks.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2017 - Copyright Red Hat Inc
  4. Authors:
  5. Patrick Uiterwijk <puiterwijk@redhat.com>
  6. """
  7. from __future__ import absolute_import, unicode_literals
  8. import collections
  9. import datetime
  10. import hashlib
  11. import os
  12. import os.path
  13. import subprocess
  14. import time
  15. import arrow
  16. import pygit2
  17. import six
  18. from celery import Celery
  19. from celery.result import AsyncResult
  20. from celery.signals import after_setup_task_logger
  21. from celery.utils.log import get_task_logger
  22. from sqlalchemy.exc import SQLAlchemyError
  23. import pagure.lib.git
  24. import pagure.lib.git_auth
  25. import pagure.lib.link
  26. import pagure.lib.model
  27. import pagure.lib.query
  28. import pagure.lib.repo
  29. import pagure.utils
  30. from pagure.config import config as pagure_config
  31. from pagure.lib.tasks_utils import pagure_task
  32. from pagure.utils import get_parent_repo_path
  33. # logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})
  34. _log = get_task_logger(__name__)
  35. if os.environ.get("PAGURE_BROKER_URL"):
  36. broker_url = os.environ["PAGURE_BROKER_URL"]
  37. elif pagure_config.get("BROKER_URL"):
  38. broker_url = pagure_config["BROKER_URL"]
  39. elif pagure_config.get("REDIS_SOCKET"):
  40. broker_url = "redis+socket://%s?virtual_host=%d" % (
  41. pagure_config["REDIS_SOCKET"],
  42. pagure_config["REDIS_DB"],
  43. )
  44. elif "REDIS_HOST" in pagure_config and "REDIS_PORT" in pagure_config:
  45. broker_url = "redis://%s:%d/%d" % (
  46. pagure_config["REDIS_HOST"],
  47. pagure_config["REDIS_PORT"],
  48. pagure_config["REDIS_DB"],
  49. )
  50. conn = Celery("tasks", broker=broker_url, backend=broker_url)
  51. conn.conf.update(pagure_config["CELERY_CONFIG"])
  52. @after_setup_task_logger.connect
  53. def augment_celery_log(**kwargs):
  54. pagure.utils.set_up_logging(force=True)
  55. def get_result(uuid):
  56. """Returns the AsyncResult object for a given task.
  57. :arg uuid: the unique identifier of the task to retrieve.
  58. :type uuid: str
  59. :return: celery.result.AsyncResult
  60. """
  61. return AsyncResult(uuid, conn.backend)
  62. def ret(endpoint, **kwargs):
  63. toret = {"endpoint": endpoint}
  64. toret.update(kwargs)
  65. return toret
  66. @conn.task(queue=pagure_config.get("GITOLITE_CELERY_QUEUE", None), bind=True)
  67. @pagure_task
  68. def generate_gitolite_acls(
  69. self, session, namespace=None, name=None, user=None, group=None
  70. ):
  71. """Generate the gitolite configuration file either entirely or for a
  72. specific project.
  73. :arg session: SQLAlchemy session object
  74. :type session: sqlalchemy.orm.session.Session
  75. :kwarg namespace: the namespace of the project
  76. :type namespace: None or str
  77. :kwarg name: the name of the project
  78. :type name: None or str
  79. :kwarg user: the user of the project, only set if the project is a fork
  80. :type user: None or str
  81. :kwarg group: the group to refresh the members of
  82. :type group: None or str
  83. """
  84. project = None
  85. if name and name != -1:
  86. project = pagure.lib.query._get_project(
  87. session, namespace=namespace, name=name, user=user
  88. )
  89. elif name == -1:
  90. project = name
  91. helper = pagure.lib.git_auth.get_git_auth_helper()
  92. _log.debug("Got helper: %s", helper)
  93. group_obj = None
  94. if group:
  95. group_obj = pagure.lib.query.search_groups(session, group_name=group)
  96. _log.debug(
  97. "Calling helper: %s with arg: project=%s, group=%s",
  98. helper,
  99. project,
  100. group_obj,
  101. )
  102. helper.generate_acls(project=project, group=group_obj)
  103. pagure.lib.query.update_read_only_mode(session, project, read_only=False)
  104. try:
  105. session.commit()
  106. _log.debug("Project %s is no longer in Read Only Mode", project)
  107. except SQLAlchemyError:
  108. session.rollback()
  109. _log.exception("Failed to unmark read_only for: %s project", project)
  110. @conn.task(queue=pagure_config.get("GITOLITE_CELERY_QUEUE", None), bind=True)
  111. @pagure_task
  112. def gitolite_post_compile_only(self, session):
  113. """Do gitolite post-processing only. Most importantly, this processes SSH
  114. keys used by gitolite. This is an optimization task that's supposed to be
  115. used if you only need to run `gitolite trigger POST_COMPILE` without
  116. touching any other gitolite configuration
  117. """
  118. helper = pagure.lib.git_auth.get_git_auth_helper()
  119. _log.debug("Got helper: %s", helper)
  120. if hasattr(helper, "post_compile_only"):
  121. helper.post_compile_only()
  122. else:
  123. helper.generate_acls(project=None)
  124. @conn.task(queue=pagure_config.get("GITOLITE_CELERY_QUEUE", None), bind=True)
  125. @pagure_task
  126. def delete_project(
  127. self, session, namespace=None, name=None, user=None, action_user=None
  128. ):
  129. """Delete a project in pagure.
  130. This is achieved in three steps:
  131. - Remove the project from gitolite.conf
  132. - Remove the git repositories on disk
  133. - Remove the project from the DB
  134. :arg session: SQLAlchemy session object
  135. :type session: sqlalchemy.orm.session.Session
  136. :kwarg namespace: the namespace of the project
  137. :type namespace: None or str
  138. :kwarg name: the name of the project
  139. :type name: None or str
  140. :kwarg user: the user of the project, only set if the project is a fork
  141. :type user: None or str
  142. :kwarg action_user: the user deleting the project
  143. :type action_user: None or str
  144. """
  145. project = pagure.lib.query._get_project(
  146. session, namespace=namespace, name=name, user=user
  147. )
  148. if not project:
  149. raise RuntimeError(
  150. "Project: %s/%s from user: %s not found in the DB"
  151. % (namespace, name, user)
  152. )
  153. # Remove the project from gitolite.conf
  154. helper = pagure.lib.git_auth.get_git_auth_helper()
  155. _log.debug("Got helper: %s", helper)
  156. _log.debug(
  157. "Calling helper: %s with arg: project=%s", helper, project.fullname
  158. )
  159. helper.remove_acls(session=session, project=project)
  160. # Remove the git repositories on disk
  161. pagure.lib.git.delete_project_repos(project)
  162. # Remove the project from the DB
  163. username = project.user.user
  164. try:
  165. project_json = project.to_json(public=True)
  166. session.delete(project)
  167. session.commit()
  168. pagure.lib.notify.log(
  169. project,
  170. topic="project.deleted",
  171. msg=dict(project=project_json, agent=action_user),
  172. )
  173. except SQLAlchemyError:
  174. session.rollback()
  175. _log.exception(
  176. "Failed to delete project: %s from the DB", project.fullname
  177. )
  178. return ret("ui_ns.view_user", username=username)
  179. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  180. @pagure_task
  181. def create_project(
  182. self,
  183. session,
  184. username,
  185. namespace,
  186. name,
  187. add_readme,
  188. ignore_existing_repo,
  189. default_branch=None,
  190. ):
  191. """Create a project.
  192. :arg session: SQLAlchemy session object
  193. :type session: sqlalchemy.orm.session.Session
  194. :kwarg username: the user creating the project
  195. :type user: str
  196. :kwarg namespace: the namespace of the project
  197. :type namespace: str
  198. :kwarg name: the name of the project
  199. :type name: str
  200. :kwarg add_readme: a boolean specifying if the project should be
  201. created with a README file or not
  202. :type add_readme: bool
  203. :kwarg ignore_existing_repo: a boolean specifying whether the creation
  204. of the project should fail if the repo exists on disk or not
  205. :type ignore_existing_repo: bool
  206. :kwarg default_branch: the name of the default branch to create and set
  207. as default.
  208. :type default_branch: str or None
  209. """
  210. project = pagure.lib.query._get_project(
  211. session, namespace=namespace, name=name
  212. )
  213. userobj = pagure.lib.query.search_user(session, username=username)
  214. # Add the readme file if it was asked
  215. templ = None
  216. if project.is_fork:
  217. templ = pagure_config.get("FORK_TEMPLATE_PATH")
  218. else:
  219. templ = pagure_config.get("PROJECT_TEMPLATE_PATH")
  220. if templ:
  221. if not os.path.exists(templ):
  222. _log.warning(
  223. "Invalid git template configured: %s, not found on disk", templ
  224. )
  225. templ = None
  226. else:
  227. _log.debug(" Using template at: %s", templ)
  228. # There is a risk for a race-condition here between when the repo is
  229. # created and when the README gets added. However, this risk is small
  230. # enough that we will keep this as is for now (esp since it fixes the
  231. # situation where deleting the project raised an error if it was in the
  232. # middle of the lock)
  233. try:
  234. with project.lock("WORKER"):
  235. pagure.lib.git.create_project_repos(
  236. project,
  237. templ,
  238. ignore_existing_repo,
  239. )
  240. except Exception:
  241. session.delete(project)
  242. session.commit()
  243. raise
  244. if default_branch:
  245. path = project.repopath("main")
  246. repo_obj = pygit2.Repository(path)
  247. repo_obj.set_head("refs/heads/%s" % default_branch)
  248. if add_readme:
  249. with project.lock("WORKER"):
  250. with pagure.lib.git.TemporaryClone(
  251. project, "main", "add_readme"
  252. ) as tempclone:
  253. temp_gitrepo = tempclone.repo
  254. if default_branch:
  255. temp_gitrepo.set_head("refs/heads/%s" % default_branch)
  256. # Add README file
  257. author = userobj.fullname or userobj.user
  258. author_email = userobj.default_email
  259. if six.PY2:
  260. author = author.encode("utf-8")
  261. author_email = author_email.encode("utf-8")
  262. author = pygit2.Signature(author, author_email)
  263. content = "# %s\n\n%s" % (name, project.description)
  264. readme_file = os.path.join(temp_gitrepo.workdir, "README.md")
  265. with open(readme_file, "wb") as stream:
  266. stream.write(content.encode("utf-8"))
  267. temp_gitrepo.index.add_all()
  268. temp_gitrepo.index.write()
  269. tree = temp_gitrepo.index.write_tree()
  270. temp_gitrepo.create_commit(
  271. "HEAD", author, author, "Added the README", tree, []
  272. )
  273. master_ref = temp_gitrepo.lookup_reference("HEAD").resolve()
  274. tempclone.push("pagure", master_ref.name, internal="yes")
  275. task = generate_gitolite_acls.delay(
  276. namespace=project.namespace,
  277. name=project.name,
  278. user=project.user.user if project.is_fork else None,
  279. )
  280. _log.info("Refreshing gitolite config queued in task: %s", task.id)
  281. return ret("ui_ns.view_repo", repo=name, namespace=namespace)
  282. @conn.task(queue=pagure_config.get("SLOW_CELERY_QUEUE", None), bind=True)
  283. @pagure_task
  284. def update_git(
  285. self, session, name, namespace, user, ticketuid=None, requestuid=None
  286. ):
  287. """Update the JSON representation of either a ticket or a pull-request
  288. depending on the argument specified.
  289. """
  290. project = pagure.lib.query._get_project(
  291. session, namespace=namespace, name=name, user=user
  292. )
  293. project_lock = "WORKER"
  294. if ticketuid is not None:
  295. project_lock = "WORKER_TICKET"
  296. elif requestuid is not None:
  297. project_lock = "WORKER_REQUEST"
  298. with project.lock(project_lock):
  299. if ticketuid is not None:
  300. obj = pagure.lib.query.get_issue_by_uid(session, ticketuid)
  301. elif requestuid is not None:
  302. obj = pagure.lib.query.get_request_by_uid(session, requestuid)
  303. else:
  304. raise NotImplementedError("No ticket ID or request ID provided")
  305. if obj is None:
  306. raise Exception("Unable to find object")
  307. result = pagure.lib.git._update_git(obj, project)
  308. return result
  309. @conn.task(queue=pagure_config.get("SLOW_CELERY_QUEUE", None), bind=True)
  310. @pagure_task
  311. def clean_git(self, session, name, namespace, user, obj_repotype, obj_uid):
  312. """Remove the JSON representation of a ticket on the git repository
  313. for tickets.
  314. """
  315. project = pagure.lib.query._get_project(
  316. session, namespace=namespace, name=name, user=user
  317. )
  318. with project.lock("WORKER_TICKET"):
  319. result = pagure.lib.git._clean_git(project, obj_repotype, obj_uid)
  320. return result
  321. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  322. @pagure_task
  323. def update_file_in_git(
  324. self,
  325. session,
  326. name,
  327. namespace,
  328. user,
  329. branch,
  330. branchto,
  331. filename,
  332. content,
  333. message,
  334. username,
  335. email,
  336. ):
  337. """Update a file in the specified git repo."""
  338. userobj = pagure.lib.query.search_user(session, username=username)
  339. project = pagure.lib.query._get_project(
  340. session, namespace=namespace, name=name, user=user
  341. )
  342. with project.lock("WORKER"):
  343. pagure.lib.git._update_file_in_git(
  344. project,
  345. branch,
  346. branchto,
  347. filename,
  348. content,
  349. message,
  350. userobj,
  351. email,
  352. )
  353. return ret(
  354. "ui_ns.view_commits",
  355. repo=project.name,
  356. username=user,
  357. namespace=namespace,
  358. branchname=branchto,
  359. )
  360. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  361. @pagure_task
  362. def delete_branch(self, session, name, namespace, user, branchname):
  363. """Delete a branch from a git repo."""
  364. project = pagure.lib.query._get_project(
  365. session, namespace=namespace, name=name, user=user
  366. )
  367. with project.lock("WORKER"):
  368. repo_obj = pygit2.Repository(pagure.utils.get_repo_path(project))
  369. try:
  370. branch = repo_obj.lookup_branch(branchname)
  371. branch.delete()
  372. except pygit2.GitError as err:
  373. _log.exception(err)
  374. return ret(
  375. "ui_ns.view_branches", repo=name, namespace=namespace, username=user
  376. )
  377. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  378. @pagure_task
  379. def fork(
  380. self,
  381. session,
  382. name,
  383. namespace,
  384. user_owner,
  385. user_forker,
  386. editbranch,
  387. editfile,
  388. ):
  389. """Forks the specified project for the specified user.
  390. :arg namespace: the namespace of the project
  391. :type namespace: str
  392. :arg name: the name of the project
  393. :type name: str
  394. :arg user_owner: the user of which the project is forked, only set
  395. if the project is already a fork
  396. :type user_owner: str
  397. :arg user_forker: the user forking the project
  398. :type user_forker: str
  399. :kwarg editbranch: the name of the branch in which the user asked to
  400. edit a file
  401. :type editbranch: str
  402. :kwarg editfile: the file the user asked to edit
  403. :type editfile: str
  404. """
  405. repo_from = pagure.lib.query._get_project(
  406. session, namespace=namespace, name=name, user=user_owner
  407. )
  408. repo_to = pagure.lib.query._get_project(
  409. session, namespace=namespace, name=name, user=user_forker
  410. )
  411. with repo_to.lock("WORKER"):
  412. pagure.lib.git.create_project_repos(repo_to, None, False)
  413. with pagure.lib.git.TemporaryClone(
  414. repo_from, "main", "fork"
  415. ) as tempclone:
  416. for branchname in tempclone.repo.branches.remote:
  417. if (
  418. branchname.startswith("origin/")
  419. and branchname != "origin/HEAD"
  420. ):
  421. locbranch = branchname[len("origin/") :]
  422. if locbranch in tempclone.repo.branches.local:
  423. continue
  424. branch = tempclone.repo.branches.remote.get(branchname)
  425. tempclone.repo.branches.local.create(
  426. locbranch, branch.peel()
  427. )
  428. tempclone.change_project_association(repo_to)
  429. tempclone.mirror("pagure", internal_no_hooks="yes")
  430. if not repo_to.private:
  431. # Create the git-daemon-export-ok file on the clone
  432. http_clone_file = os.path.join(
  433. repo_to.repopath("main"), "git-daemon-export-ok"
  434. )
  435. if not os.path.exists(http_clone_file):
  436. with open(http_clone_file, "w"):
  437. pass
  438. # Finally set the default branch to be the same as the parent
  439. repo_from_obj = pygit2.Repository(repo_from.repopath("main"))
  440. repo_to_obj = pygit2.Repository(repo_to.repopath("main"))
  441. repo_to_obj.set_head(repo_from_obj.lookup_reference("HEAD").target)
  442. pagure.lib.notify.log(
  443. repo_to,
  444. topic="project.forked",
  445. msg=dict(project=repo_to.to_json(public=True), agent=user_forker),
  446. )
  447. _log.info("Project created, refreshing auth async")
  448. task = generate_gitolite_acls.delay(
  449. namespace=repo_to.namespace,
  450. name=repo_to.name,
  451. user=repo_to.user.user if repo_to.is_fork else None,
  452. )
  453. _log.info("Refreshing gitolite config queued in task: %s", task.id)
  454. if editfile is None:
  455. return ret(
  456. "ui_ns.view_repo",
  457. repo=name,
  458. namespace=namespace,
  459. username=user_forker,
  460. )
  461. else:
  462. return ret(
  463. "ui_ns.edit_file",
  464. repo=name,
  465. namespace=namespace,
  466. username=user_forker,
  467. branchname=editbranch,
  468. filename=editfile,
  469. )
  470. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  471. @pagure_task
  472. def pull_remote_repo(self, session, remote_git, branch_from):
  473. """Clone a remote git repository locally for remote PRs."""
  474. clonepath = pagure.utils.get_remote_repo_path(
  475. remote_git, branch_from, ignore_non_exist=True
  476. )
  477. pagure.lib.repo.PagureRepo.clone(
  478. remote_git, clonepath, checkout_branch=branch_from
  479. )
  480. return clonepath
  481. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  482. @pagure_task
  483. def refresh_remote_pr(self, session, name, namespace, user, requestid):
  484. """Refresh the local clone of a git repository used in a remote
  485. pull-request.
  486. """
  487. project = pagure.lib.query._get_project(
  488. session, namespace=namespace, name=name, user=user
  489. )
  490. request = pagure.lib.query.search_pull_requests(
  491. session, project_id=project.id, requestid=requestid
  492. )
  493. _log.debug(
  494. "refreshing remote pull-request: %s/#%s",
  495. request.project.fullname,
  496. request.id,
  497. )
  498. clonepath = pagure.utils.get_remote_repo_path(
  499. request.remote_git, request.branch_from
  500. )
  501. repo = pagure.lib.repo.PagureRepo(clonepath)
  502. repo.pull(branch=request.branch_from, force=True)
  503. refresh_pr_cache.delay(name, namespace, user)
  504. del repo
  505. return ret(
  506. "ui_ns.request_pull",
  507. username=user,
  508. namespace=namespace,
  509. repo=name,
  510. requestid=requestid,
  511. )
  512. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  513. @pagure_task
  514. def refresh_pr_cache(self, session, name, namespace, user, but_uids=None):
  515. """Refresh the merge status cached of pull-requests."""
  516. project = pagure.lib.query._get_project(
  517. session, namespace=namespace, name=name, user=user
  518. )
  519. pagure.lib.query.reset_status_pull_request(
  520. session, project, but_uids=but_uids
  521. )
  522. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  523. @pagure_task
  524. def rebase_pull_request(
  525. self, session, name, namespace, user, requestid, user_rebaser
  526. ):
  527. """Rebase a pull-request."""
  528. project = pagure.lib.query._get_project(
  529. session, namespace=namespace, name=name, user=user
  530. )
  531. _log.info("Rebase PR: %s of project: %s" % (requestid, project.fullname))
  532. with project.lock("WORKER"):
  533. request = pagure.lib.query.search_pull_requests(
  534. session, project_id=project.id, requestid=requestid
  535. )
  536. _log.debug(
  537. "Rebasing pull-request: %s#%s, uid: %s",
  538. request.project.fullname,
  539. request.id,
  540. request.uid,
  541. )
  542. pagure.lib.git.rebase_pull_request(session, request, user_rebaser)
  543. update_pull_request(request.uid, username=user_rebaser)
  544. # Schedule refresh of all opened PRs
  545. pagure.lib.query.reset_status_pull_request(session, request.project)
  546. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  547. @pagure_task
  548. def merge_pull_request(
  549. self,
  550. session,
  551. name,
  552. namespace,
  553. user,
  554. requestid,
  555. user_merger,
  556. delete_branch_after=False,
  557. ):
  558. """Merge pull-request."""
  559. project = pagure.lib.query._get_project(
  560. session, namespace=namespace, name=name, user=user
  561. )
  562. with project.lock("WORKER"):
  563. request = pagure.lib.query.search_pull_requests(
  564. session, project_id=project.id, requestid=requestid
  565. )
  566. _log.debug(
  567. "Merging pull-request: %s/#%s",
  568. request.project.fullname,
  569. request.id,
  570. )
  571. pagure.lib.git.merge_pull_request(session, request, user_merger)
  572. if delete_branch_after:
  573. _log.debug(
  574. "Will delete source branch of pull-request: %s/#%s",
  575. request.project.fullname,
  576. request.id,
  577. )
  578. owner = (
  579. request.project_from.user.username
  580. if request.project_from.parent
  581. else None
  582. )
  583. delete_branch.delay(
  584. request.project_from.name,
  585. request.project_from.namespace,
  586. owner,
  587. request.branch_from,
  588. )
  589. refresh_pr_cache.delay(name, namespace, user)
  590. return ret(
  591. "ui_ns.request_pull",
  592. repo=name,
  593. requestid=requestid,
  594. username=user,
  595. namespace=namespace,
  596. )
  597. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  598. @pagure_task
  599. def add_file_to_git(
  600. self, session, name, namespace, user, user_attacher, issueuid, filename
  601. ):
  602. """Add a file to the specified git repo."""
  603. project = pagure.lib.query._get_project(
  604. session, namespace=namespace, name=name, user=user
  605. )
  606. with project.lock("WORKER"):
  607. issue = pagure.lib.query.get_issue_by_uid(session, issueuid)
  608. user_attacher = pagure.lib.query.search_user(
  609. session, username=user_attacher
  610. )
  611. from_folder = pagure_config["ATTACHMENTS_FOLDER"]
  612. _log.info(
  613. "Adding file %s from %s to %s",
  614. filename,
  615. from_folder,
  616. project.fullname,
  617. )
  618. pagure.lib.git._add_file_to_git(
  619. project, issue, from_folder, user_attacher, filename
  620. )
  621. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  622. @pagure_task
  623. def project_dowait(self, session, name, namespace, user):
  624. """This is a task used to test the locking systems.
  625. It should never be allowed to be called in production instances, since that
  626. would allow an attacker to basically DOS a project by calling this
  627. repeatedly."""
  628. assert pagure_config.get("ALLOW_PROJECT_DOWAIT", False)
  629. project = pagure.lib.query._get_project(
  630. session, namespace=namespace, name=name, user=user
  631. )
  632. with project.lock("WORKER"):
  633. time.sleep(10)
  634. return ret(
  635. "ui_ns.view_repo", repo=name, username=user, namespace=namespace
  636. )
  637. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  638. @pagure_task
  639. def sync_pull_ref(self, session, name, namespace, user, requestid):
  640. """Synchronize a pull/ reference from the content in the forked repo,
  641. allowing local checkout of the pull-request.
  642. """
  643. project = pagure.lib.query._get_project(
  644. session, namespace=namespace, name=name, user=user
  645. )
  646. with project.lock("WORKER"):
  647. request = pagure.lib.query.search_pull_requests(
  648. session, project_id=project.id, requestid=requestid
  649. )
  650. _log.debug(
  651. "Update pull refs of: %s#%s", request.project.fullname, request.id
  652. )
  653. if request.remote:
  654. # Get the fork
  655. repopath = pagure.utils.get_remote_repo_path(
  656. request.remote_git, request.branch_from
  657. )
  658. elif request.project_from:
  659. # Get the fork
  660. repopath = pagure.utils.get_repo_path(request.project_from)
  661. else:
  662. return
  663. _log.debug(" working on the repo in: %s", repopath)
  664. repo_obj = pygit2.Repository(repopath)
  665. pagure.lib.git.update_pull_ref(request, repo_obj)
  666. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  667. @pagure_task
  668. def update_pull_request(self, session, pr_uid, username=None):
  669. """Updates a pull-request in the DB once a commit was pushed to it in
  670. git.
  671. """
  672. request = pagure.lib.query.get_request_by_uid(session, pr_uid)
  673. with request.project.lock("WORKER"):
  674. _log.debug(
  675. "Updating pull-request: %s#%s",
  676. request.project.fullname,
  677. request.id,
  678. )
  679. try:
  680. pagure.lib.git.merge_pull_request(
  681. session=session,
  682. request=request,
  683. username=username,
  684. domerge=False,
  685. )
  686. except pagure.exceptions.PagureException as err:
  687. _log.debug(err)
  688. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  689. @pagure_task
  690. def update_checksums_file(self, session, folder, filenames):
  691. """Update the checksums file in the release folder of the project."""
  692. sha_file = os.path.join(folder, "CHECKSUMS")
  693. new_file = not os.path.exists(sha_file)
  694. if not new_file:
  695. with open(sha_file) as stream:
  696. row = stream.readline().strip()
  697. if row != "# Generated and updated by pagure":
  698. # This wasn't generated by pagure, don't touch it!
  699. return
  700. for filename in filenames:
  701. algos = {"sha256": hashlib.sha256(), "sha512": hashlib.sha512()}
  702. # for each files computes the different algorythm supported
  703. with open(os.path.join(folder, filename), "rb") as stream:
  704. while True:
  705. buf = stream.read(2 * 2**10) # fmt: skip
  706. if buf:
  707. for hasher in algos.values():
  708. hasher.update(buf)
  709. else:
  710. break
  711. # Write them out to the output file
  712. with open(sha_file, "a") as stream:
  713. if new_file:
  714. stream.write("# Generated and updated by pagure\n")
  715. new_file = False
  716. for algo in sorted(algos):
  717. stream.write(
  718. "%s (%s) = %s\n"
  719. % (algo.upper(), filename, algos[algo].hexdigest())
  720. )
  721. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  722. @pagure_task
  723. def commits_author_stats(self, session, repopath):
  724. """Returns some statistics about commits made against the specified
  725. git repository.
  726. """
  727. if not os.path.exists(repopath):
  728. raise ValueError("Git repository not found.")
  729. repo_obj = pygit2.Repository(repopath)
  730. stats = collections.defaultdict(int)
  731. number_of_commits = 0
  732. authors_email = set()
  733. for commit in repo_obj.walk(
  734. repo_obj.head.peel().oid.hex, pygit2.GIT_SORT_NONE
  735. ):
  736. # For each commit record how many times each combination of name and
  737. # e-mail appears in the git history.
  738. number_of_commits += 1
  739. email = commit.author.email
  740. author = commit.author.name
  741. stats[(author, email)] += 1
  742. for (name, email), val in list(stats.items()):
  743. if not email:
  744. # Author email is missing in the git commit.
  745. continue
  746. # For each recorded user info, check if we know the e-mail address of
  747. # the user.
  748. user = pagure.lib.query.search_user(session, email=email)
  749. if user and (user.default_email != email or user.fullname != name):
  750. # We know the the user, but the name or e-mail used in Git commit
  751. # does not match their default e-mail address and full name. Let's
  752. # merge them into one record.
  753. stats.pop((name, email))
  754. stats[(user.fullname, user.default_email)] += val
  755. # Generate a list of contributors ordered by how many commits they
  756. # authored. The list consists of tuples with number of commits and people
  757. # with that number of commits. Each contributor is represented by a tuple
  758. # of name, e-mail address and avatar url.
  759. out_stats = collections.defaultdict(list)
  760. for authors, val in stats.items():
  761. authors_email.add(authors[1])
  762. out_authors = list(authors)
  763. out_authors.append(
  764. pagure.lib.query.avatar_url_from_email(authors[1], size=32)
  765. )
  766. out_stats[val].append(tuple(out_authors))
  767. out_list = [
  768. (key, out_stats[key]) for key in sorted(out_stats, reverse=True)
  769. ]
  770. return (
  771. number_of_commits,
  772. out_list,
  773. len(authors_email),
  774. commit.commit_time,
  775. )
  776. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  777. @pagure_task
  778. def commits_history_stats(self, session, repopath):
  779. """Returns the evolution of the commits made against the specified
  780. git repository.
  781. """
  782. if not os.path.exists(repopath):
  783. raise ValueError("Git repository not found.")
  784. repo_obj = pygit2.Repository(repopath)
  785. dates = collections.defaultdict(int)
  786. for commit in repo_obj.walk(
  787. repo_obj.head.peel().oid.hex, pygit2.GIT_SORT_NONE
  788. ):
  789. delta = (
  790. datetime.datetime.utcnow() - arrow.get(commit.commit_time).naive
  791. )
  792. if delta.days > 365:
  793. break
  794. dates[arrow.get(commit.commit_time).date().isoformat()] += 1
  795. return [(key, dates[key]) for key in sorted(dates)]
  796. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  797. @pagure_task
  798. def link_pr_to_ticket(self, session, pr_uid):
  799. """Link the specified pull-request against the ticket(s) mentioned in
  800. the commits of the pull-request
  801. """
  802. _log.info("LINK_PR_TO_TICKET: Linking ticket(s) to PR for: %s" % pr_uid)
  803. request = pagure.lib.query.get_request_by_uid(session, pr_uid)
  804. if not request:
  805. _log.info("LINK_PR_TO_TICKET: Not PR found for: %s" % pr_uid)
  806. return
  807. if request.remote:
  808. repopath = pagure.utils.get_remote_repo_path(
  809. request.remote_git, request.branch_from
  810. )
  811. parentpath = pagure.utils.get_repo_path(request.project)
  812. elif request.project_from:
  813. repo_from = request.project_from
  814. repopath = pagure.utils.get_repo_path(repo_from)
  815. parentpath = get_parent_repo_path(repo_from)
  816. else:
  817. _log.info(
  818. "LINK_PR_TO_TICKET: PR neither remote, nor with a "
  819. "project_from, bailing: %s" % pr_uid
  820. )
  821. return
  822. # Drop the existing commit-based relations
  823. session.query(pagure.lib.model.PrToIssue).filter(
  824. pagure.lib.model.PrToIssue.pull_request_uid == request.uid
  825. ).filter(pagure.lib.model.PrToIssue.origin == "intial_comment_pr").delete(
  826. synchronize_session="fetch"
  827. )
  828. repo_obj = pygit2.Repository(repopath)
  829. orig_repo = pygit2.Repository(parentpath)
  830. diff_commits = pagure.lib.git.diff_pull_request(
  831. session, request, repo_obj, orig_repo, with_diff=False, notify=False
  832. )
  833. _log.info(
  834. "LINK_PR_TO_TICKET: Found %s commits in that PR" % len(diff_commits)
  835. )
  836. name = request.project.name
  837. namespace = request.project.namespace
  838. user = request.project.user.user if request.project.is_fork else None
  839. for line in pagure.lib.git.read_git_lines(
  840. ["log", "--no-walk"] + [c.oid.hex for c in diff_commits] + ["--"],
  841. repopath,
  842. ):
  843. line = line.strip()
  844. for issue in pagure.lib.link.get_relation(
  845. session, name, user, namespace, line, "fixes", include_prs=False
  846. ):
  847. _log.info(
  848. "LINK_PR_TO_TICKET: Link ticket %s to PRs %s"
  849. % (issue, request)
  850. )
  851. pagure.lib.query.link_pr_issue(
  852. session, issue, request, origin="commit"
  853. )
  854. for issue in pagure.lib.link.get_relation(
  855. session, name, user, namespace, line, "relates"
  856. ):
  857. _log.info(
  858. "LINK_PR_TO_TICKET: Link ticket %s to PRs %s"
  859. % (issue, request)
  860. )
  861. pagure.lib.query.link_pr_issue(
  862. session, issue, request, origin="commit"
  863. )
  864. try:
  865. session.commit()
  866. except SQLAlchemyError:
  867. _log.exception("Could not link ticket to PR :(")
  868. session.rollback()
  869. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  870. @pagure_task
  871. def pull_request_ready_branch(self, session, namespace, name, user):
  872. repo = pagure.lib.query._get_project(
  873. session, name, user=user, namespace=namespace
  874. )
  875. repo_path = pagure.utils.get_repo_path(repo)
  876. repo_obj = pygit2.Repository(repo_path)
  877. if repo.is_fork and repo.parent:
  878. parentreponame = pagure.utils.get_repo_path(repo.parent)
  879. parent_repo_obj = pygit2.Repository(parentreponame)
  880. else:
  881. parent_repo_obj = repo_obj
  882. branches = {}
  883. if not repo_obj.is_empty and len(repo_obj.listall_branches()) > 0:
  884. branch_names = (
  885. pagure.lib.repo.PagureRepo.get_active_branches(
  886. repo_path, catch_exception=True
  887. )
  888. or repo_obj.listall_branches()
  889. )
  890. for branchname in branch_names:
  891. compare_branch = None
  892. if (
  893. not parent_repo_obj.is_empty
  894. and not parent_repo_obj.head_is_unborn
  895. ):
  896. try:
  897. if pagure.config.config.get(
  898. "PR_TARGET_MATCHING_BRANCH", False
  899. ):
  900. # find parent branch which is the longest substring of
  901. # branch that we're processing
  902. compare_branch = ""
  903. for parent_branch in parent_repo_obj.branches:
  904. if (
  905. not repo.is_fork
  906. and branchname == parent_branch
  907. ):
  908. continue
  909. if branchname.startswith(parent_branch) and len(
  910. parent_branch
  911. ) > len(compare_branch):
  912. compare_branch = parent_branch
  913. compare_branch = (
  914. compare_branch or repo_obj.head.shorthand
  915. )
  916. else:
  917. compare_branch = repo_obj.head.shorthand
  918. except pygit2.GitError:
  919. pass # let compare_branch be None
  920. # Do not compare a branch to itself
  921. if (
  922. not repo.is_fork
  923. and compare_branch
  924. and compare_branch == branchname
  925. ):
  926. continue
  927. diff_commits = None
  928. try:
  929. _, diff_commits, _ = pagure.lib.git.get_diff_info(
  930. repo_obj, parent_repo_obj, branchname, compare_branch
  931. )
  932. except pagure.exceptions.PagureException:
  933. pass
  934. if diff_commits:
  935. branches[branchname] = {
  936. "commits": len(list(diff_commits)),
  937. "target_branch": compare_branch or "master",
  938. }
  939. prs = pagure.lib.query.search_pull_requests(
  940. session, project_id_from=repo.id, status="Open"
  941. )
  942. branches_pr = {}
  943. for pr in prs:
  944. if pr.branch_from in branches:
  945. branches_pr[pr.branch_from] = "%s/pull-request/%s" % (
  946. pr.project.url_path,
  947. pr.id,
  948. )
  949. del branches[pr.branch_from]
  950. return {"new_branch": branches, "branch_w_pr": branches_pr}
  951. @conn.task(queue=pagure_config.get("MEDIUM_CELERY_QUEUE", None), bind=True)
  952. @pagure_task
  953. def git_garbage_collect(self, session, repopath):
  954. # libgit2 doesn't support "git gc" and probably never will:
  955. # https://github.com/libgit2/libgit2/issues/3247
  956. _log.info("Running 'git gc --auto' for repo %s", repopath)
  957. subprocess.check_output(["git", "gc", "--auto", "-q"], cwd=repopath)
  958. @conn.task(queue=pagure_config.get("FAST_CELERY_QUEUE", None), bind=True)
  959. @pagure_task
  960. def generate_archive(
  961. self, session, project, namespace, username, commit, tag, name, archive_fmt
  962. ):
  963. """Generate the archive of the specified project on the specified
  964. commit with the given name and archive format.
  965. Currently only support the following format: gzip and tar.gz
  966. """
  967. project = pagure.lib.query._get_project(
  968. session, namespace=namespace, name=project, user=username
  969. )
  970. _log.debug(
  971. "Generating archive for %s, commit: %s as: %s.%s",
  972. project.fullname,
  973. commit,
  974. name,
  975. archive_fmt,
  976. )
  977. pagure.lib.git.generate_archive(project, commit, tag, name, archive_fmt)
  978. if archive_fmt == "gzip":
  979. endpoint = "ui_ns.get_project_archive_gzip"
  980. elif archive_fmt == "tar":
  981. endpoint = "ui_ns.get_project_archive_tar"
  982. else:
  983. endpoint = "ui_ns.get_project_archive_tar_gz"
  984. return ret(
  985. endpoint,
  986. repo=project.name,
  987. ref=commit,
  988. name=name,
  989. namespace=project.namespace,
  990. username=project.user.user if project.is_fork else None,
  991. )
  992. @conn.task(queue=pagure_config.get("AUTHORIZED_KEYS_QUEUE", None), bind=True)
  993. @pagure_task
  994. def add_key_to_authorized_keys(self, session, ssh_folder, username, sshkey):
  995. """Add the specified key to the the `authorized_keys` file of the
  996. specified ssh folder.
  997. """
  998. if not os.path.exists(ssh_folder):
  999. _log.info("No folder '%s' found", ssh_folder)
  1000. return
  1001. fullpath = os.path.join(ssh_folder, "authorized_keys")
  1002. _log.info("Add ssh key for user %s to %s", username, fullpath)
  1003. with open(fullpath, "a") as stream:
  1004. stream.write("\n")
  1005. stream.write(
  1006. "{0} {1}".format(
  1007. pagure_config["SSH_KEYS_OPTIONS"] % {"username": username},
  1008. sshkey.strip(),
  1009. )
  1010. )
  1011. os.chmod(fullpath, 0o600)
  1012. @conn.task(queue=pagure_config.get("AUTHORIZED_KEYS_QUEUE", None), bind=True)
  1013. @pagure_task
  1014. def remove_key_from_authorized_keys(self, session, ssh_folder, sshkey):
  1015. """Remove the specified key from the the `authorized_keys` file of the
  1016. specified ssh folder.
  1017. """
  1018. if not os.path.exists(ssh_folder):
  1019. _log.info("No folder '%s' found", ssh_folder)
  1020. return
  1021. fullpath = os.path.join(ssh_folder, "authorized_keys")
  1022. _log.info("Removing ssh key in %s", fullpath)
  1023. output = []
  1024. with open(fullpath, "r") as stream:
  1025. for row in stream.readlines():
  1026. row = row.strip()
  1027. if sshkey in row:
  1028. continue
  1029. output.append(row)
  1030. with open(fullpath, "w") as stream:
  1031. stream.write("\n".join(output))
  1032. os.chmod(fullpath, 0o600)