git.py 97 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2016 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import print_function, unicode_literals
  8. # pylint: disable=too-many-branches
  9. # pylint: disable=too-many-arguments
  10. # pylint: disable=too-many-locals
  11. # pylint: disable=too-many-statements
  12. # pylint: disable=too-many-lines
  13. import datetime
  14. import json
  15. import logging
  16. import os
  17. import shutil
  18. import subprocess
  19. import requests
  20. import tempfile
  21. import tarfile
  22. import zipfile
  23. import arrow
  24. import pygit2
  25. import six
  26. from sqlalchemy.exc import SQLAlchemyError
  27. # from sqlalchemy.orm.session import Session
  28. from pygit2.remote import RemoteCollection
  29. import pagure.utils
  30. import pagure.exceptions
  31. import pagure.lib.query
  32. import pagure.lib.notify
  33. from pagure.config import config as pagure_config
  34. from pagure.lib import model
  35. from pagure.lib.repo import PagureRepo
  36. from pagure.lib import tasks
  37. import pagure.hooks
  38. # from pagure.hooks import run_project_hooks
  39. _log = logging.getLogger(__name__)
  40. def commit_to_patch(
  41. repo_obj, commits, diff_view=False, find_similar=False, separated=False
  42. ):
  43. """ For a given commit (PyGit2 commit object) of a specified git repo,
  44. returns a string representation of the changes the commit did in a
  45. format that allows it to be used as patch.
  46. :arg repo_obj: the `pygit2.Repository` object of the git repo to
  47. retrieve the commits in
  48. :type repo_obj: `pygit2.Repository`
  49. :arg commits: the list of commits to convert to path
  50. :type commits: str or list
  51. :kwarg diff_view: a boolean specifying if what is returned is a git
  52. patch or a git diff
  53. :type diff_view: boolean
  54. :kwarg find_similar: a boolean specifying if what we run find_similar
  55. on the diff to group renamed files
  56. :type find_similar: boolean
  57. :kwarg separated: a boolean specifying if the data returned should be
  58. returned as one text blob or not. If diff_view is True, then the diff
  59. are also split by file, otherwise, the different patches are returned
  60. as different text blob.
  61. :type separated: boolean
  62. :return: the patch or diff representation of the provided commits
  63. :rtype: str
  64. """
  65. if not isinstance(commits, list):
  66. commits = [commits]
  67. patch = []
  68. for cnt, commit in enumerate(commits):
  69. if commit.parents:
  70. diff = repo_obj.diff(commit.parents[0], commit)
  71. else:
  72. # First commit in the repo
  73. diff = commit.tree.diff_to_tree(swap=True)
  74. if find_similar and diff:
  75. diff.find_similar()
  76. if diff_view:
  77. if separated:
  78. for el in diff.patch.split("\ndiff --git a/"):
  79. if el and not el.startswith("diff --git a/"):
  80. patch.append("\ndiff --git a/" + el)
  81. elif el:
  82. patch.append(el)
  83. else:
  84. patch.append(diff.patch)
  85. else:
  86. subject = message = ""
  87. if "\n" in commit.message:
  88. subject, message = commit.message.split("\n", 1)
  89. else:
  90. subject = commit.message
  91. if len(commits) > 1:
  92. subject = "[PATCH %s/%s] %s" % (cnt + 1, len(commits), subject)
  93. patch.append(
  94. """From {commit} Mon Sep 17 00:00:00 2001
  95. From: {author_name} <{author_email}>
  96. Date: {date}
  97. Subject: {subject}
  98. {msg}
  99. ---
  100. {patch}
  101. """.format(
  102. commit=commit.oid.hex,
  103. author_name=commit.author.name,
  104. author_email=commit.author.email,
  105. date=datetime.datetime.utcfromtimestamp(
  106. commit.commit_time
  107. ).strftime("%b %d %Y %H:%M:%S +0000"),
  108. subject=subject,
  109. msg=message,
  110. patch=diff.patch,
  111. )
  112. )
  113. if separated:
  114. return patch
  115. else:
  116. return "".join(patch)
  117. def generate_gitolite_acls(project=None, group=None):
  118. """ Generate the gitolite configuration file.
  119. :arg project: the project of which to update the ACLs. This argument
  120. can take three values: ``-1``, ``None`` and a project.
  121. If project is ``-1``, the configuration should be refreshed for
  122. *all* projects.
  123. If project is ``None``, there no specific project to refresh
  124. but the ssh key of an user was added and updated.
  125. If project is a pagure.lib.model.Project, the configuration of
  126. this project should be updated.
  127. :type project: None, int or pagure.lib.model.Project
  128. :kwarg group: the group to refresh the members of
  129. :type group: None or str
  130. """
  131. if project != -1:
  132. task = tasks.generate_gitolite_acls.delay(
  133. namespace=project.namespace if project else None,
  134. name=project.name if project else None,
  135. user=project.user.user if project and project.is_fork else None,
  136. group=group,
  137. )
  138. else:
  139. task = tasks.generate_gitolite_acls.delay(name=-1, group=group)
  140. return task
  141. def update_git(obj, repo):
  142. """ Schedules an update_repo task after determining arguments. """
  143. ticketuid = None
  144. requestuid = None
  145. if obj.isa == "issue":
  146. ticketuid = obj.uid
  147. elif obj.isa == "pull-request":
  148. requestuid = obj.uid
  149. else:
  150. raise NotImplementedError("Unknown object type %s" % obj.isa)
  151. queued = pagure.lib.tasks.update_git.delay(
  152. repo.name,
  153. repo.namespace,
  154. repo.user.username if repo.is_fork else None,
  155. ticketuid,
  156. requestuid,
  157. )
  158. _maybe_wait(queued)
  159. return queued
  160. def _maybe_wait(result):
  161. """ Function to patch if one wants to wait for finish.
  162. This function should only ever be overridden by a few tests that depend
  163. on counting and very precise timing. """
  164. pass
  165. def _make_signature(name, email):
  166. if six.PY2:
  167. if isinstance(name, six.text_type):
  168. name = name.encode("utf-8")
  169. if isinstance(email, six.text_type):
  170. email = email.encode("utf-8")
  171. return pygit2.Signature(name=name, email=email)
  172. def _update_git(obj, repo):
  173. """ Update the given issue in its git.
  174. This method forks the provided repo, add/edit the issue whose file name
  175. is defined by the uid field of the issue and if there are additions/
  176. changes commit them and push them back to the original repo.
  177. """
  178. _log.info("Update the git repo: %s for: %s", repo.path, obj)
  179. with TemporaryClone(repo, obj.repotype, "update_git") as tempclone:
  180. if tempclone is None:
  181. # Turns out we don't have a repo for this kind of object.
  182. return
  183. newpath = tempclone.repopath
  184. new_repo = tempclone.repo
  185. file_path = os.path.join(newpath, obj.uid)
  186. # Get the current index
  187. index = new_repo.index
  188. # Are we adding files
  189. added = False
  190. if not os.path.exists(file_path):
  191. added = True
  192. # Write down what changed
  193. with open(file_path, "w") as stream:
  194. stream.write(
  195. json.dumps(
  196. obj.to_json(),
  197. sort_keys=True,
  198. indent=4,
  199. separators=(",", ": "),
  200. )
  201. )
  202. # Retrieve the list of files that changed
  203. diff = new_repo.diff()
  204. files = []
  205. for patch in diff:
  206. files.append(patch.delta.new_file.path)
  207. # Add the changes to the index
  208. if added:
  209. index.add(obj.uid)
  210. for filename in files:
  211. index.add(filename)
  212. # If not change, return
  213. if not files and not added:
  214. return
  215. # See if there is a parent to this commit
  216. parent = None
  217. try:
  218. parent = new_repo.head.get_object().oid
  219. except pygit2.GitError:
  220. pass
  221. parents = []
  222. if parent:
  223. parents.append(parent)
  224. # Author/commiter will always be this one
  225. author = _make_signature(name="pagure", email="pagure")
  226. # Actually commit
  227. new_repo.create_commit(
  228. "refs/heads/master",
  229. author,
  230. author,
  231. "Updated %s %s: %s" % (obj.isa, obj.uid, obj.title),
  232. new_repo.index.write_tree(),
  233. parents,
  234. )
  235. index.write()
  236. # And push it back
  237. tempclone.push("pagure", "master", internal="yes")
  238. def clean_git(repo, obj_repotype, obj_uid):
  239. if repo is None:
  240. return
  241. task = pagure.lib.tasks.clean_git.delay(
  242. repo.name,
  243. repo.namespace,
  244. repo.user.username if repo.is_fork else None,
  245. obj_repotype,
  246. obj_uid,
  247. )
  248. _maybe_wait(task)
  249. return task
  250. def _clean_git(repo, obj_repotype, obj_uid):
  251. """ Update the given issue remove it from its git.
  252. """
  253. _log.info("Update the git repo: %s to remove: %s", repo.path, obj_uid)
  254. with TemporaryClone(repo, obj_repotype, "clean_git") as tempclone:
  255. if tempclone is None:
  256. # This repo is not tracked on disk
  257. return
  258. newpath = tempclone.repopath
  259. new_repo = tempclone.repo
  260. file_path = os.path.join(newpath, obj_uid)
  261. # Get the current index
  262. index = new_repo.index
  263. # Are we adding files
  264. if not os.path.exists(file_path):
  265. return
  266. # Remove the file
  267. os.unlink(file_path)
  268. # Add the changes to the index
  269. index.remove(obj_uid)
  270. # See if there is a parent to this commit
  271. parent = None
  272. if not new_repo.is_empty:
  273. parent = new_repo.head.get_object().oid
  274. parents = []
  275. if parent:
  276. parents.append(parent)
  277. # Author/commiter will always be this one
  278. author = _make_signature(name="pagure", email="pagure")
  279. # Actually commit
  280. new_repo.create_commit(
  281. "refs/heads/master",
  282. author,
  283. author,
  284. "Removed object %s: %s" % (obj_repotype, obj_uid),
  285. new_repo.index.write_tree(),
  286. parents,
  287. )
  288. index.write()
  289. master_ref = new_repo.lookup_reference("HEAD").resolve().name
  290. tempclone.push("pagure", master_ref, internal="yes")
  291. def get_user_from_json(session, jsondata, key="user"):
  292. """ From the given json blob, retrieve the user info and search for it
  293. in the db and create the user if it does not already exist.
  294. """
  295. user = None
  296. username = fullname = useremails = default_email = None
  297. data = jsondata.get(key, None)
  298. if data:
  299. username = data.get("name")
  300. fullname = data.get("fullname")
  301. useremails = data.get("emails")
  302. default_email = data.get("default_email")
  303. if not default_email and useremails:
  304. default_email = useremails[0]
  305. if not username and not useremails:
  306. return
  307. user = pagure.lib.query.search_user(session, username=username)
  308. if not user:
  309. for email in useremails:
  310. user = pagure.lib.query.search_user(session, email=email)
  311. if user:
  312. break
  313. if not user:
  314. user = pagure.lib.query.set_up_user(
  315. session=session,
  316. username=username,
  317. fullname=fullname or username,
  318. default_email=default_email,
  319. emails=useremails,
  320. keydir=pagure_config.get("GITOLITE_KEYDIR", None),
  321. )
  322. session.commit()
  323. return user
  324. def get_project_from_json(session, jsondata):
  325. """ From the given json blob, retrieve the project info and search for
  326. it in the db and create the projec if it does not already exist.
  327. """
  328. project = None
  329. user = get_user_from_json(session, jsondata)
  330. name = jsondata.get("name")
  331. namespace = jsondata.get("namespace")
  332. project_user = None
  333. if jsondata.get("parent"):
  334. project_user = user.username
  335. project = pagure.lib.query._get_project(
  336. session, name, user=project_user, namespace=namespace
  337. )
  338. if not project:
  339. parent = None
  340. if jsondata.get("parent"):
  341. parent = get_project_from_json(session, jsondata.get("parent"))
  342. pagure.lib.query.fork_project(
  343. session=session, repo=parent, user=user.username
  344. )
  345. else:
  346. pagure.lib.query.new_project(
  347. session,
  348. user=user.username,
  349. name=name,
  350. namespace=namespace,
  351. repospanner_region=None,
  352. description=jsondata.get("description"),
  353. parent_id=parent.id if parent else None,
  354. blacklist=pagure_config.get("BLACKLISTED_PROJECTS", []),
  355. allowed_prefix=pagure_config.get("ALLOWED_PREFIX", []),
  356. prevent_40_chars=pagure_config.get(
  357. "OLD_VIEW_COMMIT_ENABLED", False
  358. ),
  359. )
  360. session.commit()
  361. project = pagure.lib.query._get_project(
  362. session, name, user=user.username, namespace=namespace
  363. )
  364. tags = jsondata.get("tags", None)
  365. if tags:
  366. pagure.lib.query.add_tag_obj(
  367. session, project, tags=tags, user=user.username
  368. )
  369. return project
  370. def update_custom_field_from_json(session, repo, issue, json_data):
  371. """ Update the custom fields according to the custom fields of
  372. the issue. If the custom field is not present for the repo in
  373. it's settings, this will create them.
  374. :arg session: the session to connect to the database with.
  375. :arg repo: the sqlalchemy object of the project
  376. :arg issue: the sqlalchemy object of the issue
  377. :arg json_data: the json representation of the issue taken from the git
  378. and used to update the data in the database.
  379. """
  380. # Update custom key value, if present
  381. custom_fields = json_data.get("custom_fields")
  382. if not custom_fields:
  383. return
  384. current_keys = []
  385. for key in repo.issue_keys:
  386. current_keys.append(key.name)
  387. for new_key in custom_fields:
  388. if new_key["name"] not in current_keys:
  389. issuekey = model.IssueKeys(
  390. project_id=repo.id,
  391. name=new_key["name"],
  392. key_type=new_key["key_type"],
  393. )
  394. try:
  395. session.add(issuekey)
  396. session.commit()
  397. except SQLAlchemyError:
  398. session.rollback()
  399. continue
  400. # The key should be present in the database now
  401. key_obj = pagure.lib.query.get_custom_key(
  402. session, repo, new_key["name"]
  403. )
  404. value = new_key.get("value")
  405. if value:
  406. value = value.strip()
  407. pagure.lib.query.set_custom_key_value(
  408. session, issue=issue, key=key_obj, value=value
  409. )
  410. try:
  411. session.commit()
  412. except SQLAlchemyError:
  413. session.rollback()
  414. def update_ticket_from_git(
  415. session, reponame, namespace, username, issue_uid, json_data, agent
  416. ):
  417. """ Update the specified issue (identified by its unique identifier)
  418. with the data present in the json blob provided.
  419. :arg session: the session to connect to the database with.
  420. :arg repo: the name of the project to update
  421. :arg namespace: the namespace of the project to update
  422. :arg username: the username of the project to update (if the project
  423. is a fork)
  424. :arg issue_uid: the unique identifier of the issue to update
  425. :arg json_data: the json representation of the issue taken from the git
  426. and used to update the data in the database.
  427. :arg agent: the username of the person who pushed the changes (and thus
  428. is assumed did the action).
  429. """
  430. repo = pagure.lib.query._get_project(
  431. session, reponame, user=username, namespace=namespace
  432. )
  433. if not repo:
  434. raise pagure.exceptions.PagureException(
  435. "Unknown repo %s of username: %s in namespace: %s"
  436. % (reponame, username, namespace)
  437. )
  438. user = get_user_from_json(session, json_data)
  439. # rely on the agent provided, but if something goes wrong, behave as
  440. # ticket creator
  441. agent = pagure.lib.query.search_user(session, username=agent) or user
  442. issue = pagure.lib.query.get_issue_by_uid(session, issue_uid=issue_uid)
  443. messages = []
  444. if not issue:
  445. # Create new issue
  446. pagure.lib.query.new_issue(
  447. session,
  448. repo=repo,
  449. title=json_data.get("title"),
  450. content=json_data.get("content"),
  451. priority=json_data.get("priority"),
  452. user=user.username,
  453. issue_id=json_data.get("id"),
  454. issue_uid=issue_uid,
  455. private=json_data.get("private"),
  456. status=json_data.get("status"),
  457. close_status=json_data.get("close_status"),
  458. date_created=datetime.datetime.utcfromtimestamp(
  459. float(json_data.get("date_created"))
  460. ),
  461. notify=False,
  462. )
  463. else:
  464. # Edit existing issue
  465. msgs = pagure.lib.query.edit_issue(
  466. session,
  467. issue=issue,
  468. user=agent.username,
  469. title=json_data.get("title"),
  470. content=json_data.get("content"),
  471. priority=json_data.get("priority"),
  472. status=json_data.get("status"),
  473. close_status=json_data.get("close_status"),
  474. private=json_data.get("private"),
  475. )
  476. if msgs:
  477. messages.extend(msgs)
  478. session.commit()
  479. issue = pagure.lib.query.get_issue_by_uid(session, issue_uid=issue_uid)
  480. update_custom_field_from_json(
  481. session, repo=repo, issue=issue, json_data=json_data
  482. )
  483. # Update milestone
  484. milestone = json_data.get("milestone")
  485. # If milestone is not in the repo settings, add it
  486. if milestone:
  487. if milestone.strip() not in repo.milestones:
  488. try:
  489. tmp_milestone = repo.milestones.copy()
  490. tmp_milestone[milestone.strip()] = None
  491. repo.milestones = tmp_milestone
  492. session.add(repo)
  493. session.commit()
  494. except SQLAlchemyError:
  495. session.rollback()
  496. try:
  497. msgs = pagure.lib.query.edit_issue(
  498. session,
  499. issue=issue,
  500. user=agent.username,
  501. milestone=milestone,
  502. title=json_data.get("title"),
  503. content=json_data.get("content"),
  504. status=json_data.get("status"),
  505. close_status=json_data.get("close_status"),
  506. private=json_data.get("private"),
  507. )
  508. if msgs:
  509. messages.extend(msgs)
  510. except SQLAlchemyError:
  511. session.rollback()
  512. # Update close_status
  513. close_status = json_data.get("close_status")
  514. if close_status:
  515. if close_status.strip() not in repo.close_status:
  516. try:
  517. repo.close_status.append(close_status.strip())
  518. session.add(repo)
  519. session.commit()
  520. except SQLAlchemyError:
  521. session.rollback()
  522. # Update tags
  523. tags = json_data.get("tags", [])
  524. msgs = pagure.lib.query.update_tags(
  525. session, issue, tags, username=user.user
  526. )
  527. if msgs:
  528. messages.extend(msgs)
  529. # Update assignee
  530. assignee = get_user_from_json(session, json_data, key="assignee")
  531. if assignee:
  532. msg = pagure.lib.query.add_issue_assignee(
  533. session, issue, assignee.username, user=agent.user, notify=False
  534. )
  535. if msg:
  536. messages.append(msg)
  537. # Update depends
  538. depends = json_data.get("depends", [])
  539. msgs = pagure.lib.query.update_dependency_issue(
  540. session, issue.project, issue, depends, username=agent.user
  541. )
  542. if msgs:
  543. messages.extend(msgs)
  544. # Update blocks
  545. blocks = json_data.get("blocks", [])
  546. msgs = pagure.lib.query.update_blocked_issue(
  547. session, issue.project, issue, blocks, username=agent.user
  548. )
  549. if msgs:
  550. messages.extend(msgs)
  551. for comment in json_data["comments"]:
  552. usercomment = get_user_from_json(session, comment)
  553. commentobj = pagure.lib.query.get_issue_comment_by_user_and_comment(
  554. session, issue_uid, usercomment.id, comment["comment"]
  555. )
  556. if not commentobj:
  557. pagure.lib.query.add_issue_comment(
  558. session,
  559. issue=issue,
  560. comment=comment["comment"],
  561. user=usercomment.username,
  562. notify=False,
  563. date_created=datetime.datetime.fromtimestamp(
  564. float(comment["date_created"])
  565. ),
  566. )
  567. if messages:
  568. pagure.lib.query.add_metadata_update_notif(
  569. session=session, obj=issue, messages=messages, user=agent.username
  570. )
  571. session.commit()
  572. def update_request_from_git(
  573. session, reponame, namespace, username, request_uid, json_data
  574. ):
  575. """ Update the specified request (identified by its unique identifier)
  576. with the data present in the json blob provided.
  577. :arg session: the session to connect to the database with.
  578. :arg repo: the name of the project to update
  579. :arg username: the username to find the repo, is not None for forked
  580. projects
  581. :arg request_uid: the unique identifier of the issue to update
  582. :arg json_data: the json representation of the issue taken from the git
  583. and used to update the data in the database.
  584. """
  585. repo = pagure.lib.query._get_project(
  586. session, reponame, user=username, namespace=namespace
  587. )
  588. if not repo:
  589. raise pagure.exceptions.PagureException(
  590. "Unknown repo %s of username: %s in namespace: %s"
  591. % (reponame, username, namespace)
  592. )
  593. user = get_user_from_json(session, json_data)
  594. request = pagure.lib.query.get_request_by_uid(
  595. session, request_uid=request_uid
  596. )
  597. if not request:
  598. repo_from = get_project_from_json(session, json_data.get("repo_from"))
  599. repo_to = get_project_from_json(session, json_data.get("project"))
  600. status = json_data.get("status")
  601. if pagure.utils.is_true(status):
  602. status = "Open"
  603. elif pagure.utils.is_true(status, ["false"]):
  604. status = "Merged"
  605. # Create new request
  606. pagure.lib.query.new_pull_request(
  607. session,
  608. repo_from=repo_from,
  609. branch_from=json_data.get("branch_from"),
  610. repo_to=repo_to if repo_to else None,
  611. remote_git=json_data.get("remote_git"),
  612. branch_to=json_data.get("branch"),
  613. title=json_data.get("title"),
  614. user=user.username,
  615. requestuid=json_data.get("uid"),
  616. requestid=json_data.get("id"),
  617. status=status,
  618. notify=False,
  619. )
  620. session.commit()
  621. request = pagure.lib.query.get_request_by_uid(
  622. session, request_uid=request_uid
  623. )
  624. # Update start and stop commits
  625. request.commit_start = json_data.get("commit_start")
  626. request.commit_stop = json_data.get("commit_stop")
  627. # Update assignee
  628. assignee = get_user_from_json(session, json_data, key="assignee")
  629. if assignee:
  630. pagure.lib.query.add_pull_request_assignee(
  631. session, request, assignee.username, user=user.user
  632. )
  633. for comment in json_data["comments"]:
  634. user = get_user_from_json(session, comment)
  635. commentobj = pagure.lib.query.get_request_comment(
  636. session, request_uid, comment["id"]
  637. )
  638. if not commentobj:
  639. pagure.lib.query.add_pull_request_comment(
  640. session,
  641. request,
  642. commit=comment["commit"],
  643. tree_id=comment.get("tree_id") or None,
  644. filename=comment["filename"],
  645. row=comment["line"],
  646. comment=comment["comment"],
  647. user=user.username,
  648. notify=False,
  649. )
  650. session.commit()
  651. def _add_file_to_git(repo, issue, attachmentfolder, user, filename):
  652. """ Add a given file to the specified ticket git repository.
  653. :arg repo: the Project object from the database
  654. :arg attachmentfolder: the folder on the filesystem where the attachments
  655. are stored
  656. :arg ticketfolder: the folder on the filesystem where the git repo for
  657. tickets are stored
  658. :arg user: the user object with its username and email
  659. :arg filename: the name of the file to save
  660. """
  661. with TemporaryClone(repo, issue.repotype, "add_file_to_git") as tempclone:
  662. newpath = tempclone.repopath
  663. new_repo = tempclone.repo
  664. folder_path = os.path.join(newpath, "files")
  665. file_path = os.path.join(folder_path, filename)
  666. # Get the current index
  667. index = new_repo.index
  668. # Are we adding files
  669. if os.path.exists(file_path):
  670. # File exists, remove the clone and return
  671. shutil.rmtree(newpath)
  672. return os.path.join("files", filename)
  673. if not os.path.exists(folder_path):
  674. os.mkdir(folder_path)
  675. # Copy from attachments directory
  676. src = os.path.join(attachmentfolder, repo.fullname, "files", filename)
  677. shutil.copyfile(src, file_path)
  678. # Retrieve the list of files that changed
  679. diff = new_repo.diff()
  680. files = [patch.new_file_path for patch in diff]
  681. # Add the changes to the index
  682. index.add(os.path.join("files", filename))
  683. for filename in files:
  684. index.add(filename)
  685. # See if there is a parent to this commit
  686. parent = None
  687. try:
  688. parent = new_repo.head.get_object().oid
  689. except pygit2.GitError:
  690. pass
  691. parents = []
  692. if parent:
  693. parents.append(parent)
  694. # Author/commiter will always be this one
  695. author = _make_signature(name=user.username, email=user.default_email)
  696. # Actually commit
  697. new_repo.create_commit(
  698. "refs/heads/master",
  699. author,
  700. author,
  701. "Add file %s to ticket %s: %s"
  702. % (filename, issue.uid, issue.title),
  703. new_repo.index.write_tree(),
  704. parents,
  705. )
  706. index.write()
  707. master_ref = new_repo.lookup_reference("HEAD").resolve()
  708. tempclone.push(user.username, master_ref.name)
  709. return os.path.join("files", filename)
  710. class TemporaryClone(object):
  711. _project = None
  712. _action = None
  713. _repotype = None
  714. _origpath = None
  715. _origrepopath = None
  716. repopath = None
  717. repo = None
  718. def __init__(self, project, repotype, action, path=None, parent=None):
  719. """ Initializes a TempoaryClone instance.
  720. Args:
  721. project (model.Project): A project instance
  722. repotype (string): The type of repo to clone, one of:
  723. main, docs, requests, tickets
  724. action (string): Type of action performing, used in the
  725. temporary directory name
  726. path (string or None): the path to clone, allows cloning, for
  727. example remote git repo for remote PRs instead of the
  728. default one
  729. parent (string or None): Adds this directory to the path in
  730. which the project is cloned
  731. """
  732. if repotype not in pagure.lib.query.get_repotypes():
  733. raise NotImplementedError("Repotype %s not known" % repotype)
  734. self._project = project
  735. self._repotype = repotype
  736. self._action = action
  737. self._path = path
  738. self._parent = parent
  739. def __enter__(self):
  740. """ Enter the context manager, creating the clone. """
  741. self.repopath = tempfile.mkdtemp(prefix="pagure-%s-" % self._action)
  742. self._origrepopath = self.repopath
  743. if self._parent:
  744. self.repopath = os.path.join(self.repopath, self._parent)
  745. os.makedirs(self.repopath)
  746. if not self._project.is_on_repospanner:
  747. # This is the simple case. Just do a local clone
  748. # use either the specified path or the use the path of the
  749. # specified project
  750. self._origpath = self._path or self._project.repopath(
  751. self._repotype
  752. )
  753. if self._origpath is None:
  754. # No repository of this type
  755. # 'main' is already caught and returns an error in repopath()
  756. return None
  757. if not os.path.exists(self._origpath):
  758. return None
  759. pygit2.clone_repository(self._origpath, self.repopath)
  760. # Because for whatever reason, one pygit2.Repository is not
  761. # equal to another.... The pygit2.Repository returned from
  762. # pygit2.clone_repository does not have the "branches" attribute.
  763. self.repo = pygit2.Repository(self.repopath)
  764. self._origrepo = pygit2.Repository(self._origpath)
  765. else:
  766. repourl, regioninfo = self._project.repospanner_repo_info(
  767. self._repotype
  768. )
  769. command = [
  770. "git",
  771. "-c",
  772. "protocol.ext.allow=always",
  773. "clone",
  774. "ext::%s %s"
  775. % (
  776. pagure_config["REPOBRIDGE_BINARY"],
  777. self._project._repospanner_repo_name(self._repotype),
  778. ),
  779. self.repopath,
  780. ]
  781. environ = os.environ.copy()
  782. environ.update(
  783. {
  784. "USER": "pagure",
  785. "REPOBRIDGE_CONFIG": ":environment:",
  786. "REPOBRIDGE_BASEURL": regioninfo["url"],
  787. "REPOBRIDGE_CA": regioninfo["ca"],
  788. "REPOBRIDGE_CERT": regioninfo["push_cert"]["cert"],
  789. "REPOBRIDGE_KEY": regioninfo["push_cert"]["key"],
  790. }
  791. )
  792. with open(os.devnull, "w") as devnull:
  793. subprocess.check_call(
  794. command,
  795. stdout=devnull,
  796. stderr=subprocess.STDOUT,
  797. env=environ,
  798. )
  799. self.repo = pygit2.Repository(self.repopath)
  800. # Make sure that all remote refs are mapped to local ones.
  801. headname = None
  802. if not self.repo.is_empty and not self.repo.head_is_unborn:
  803. headname = self.repo.head.shorthand
  804. # Sync up all the references, branches and PR heads
  805. for ref in self._origrepo.listall_references():
  806. if ref.startswith("refs/heads/"):
  807. localname = ref.replace("refs/heads/", "")
  808. if localname in (headname, "HEAD"):
  809. # This gets checked out by default
  810. continue
  811. branch = self.repo.branches.remote.get("origin/%s" % localname)
  812. self.repo.branches.local.create(localname, branch.get_object())
  813. elif ref.startswith("refs/pull/"):
  814. reference = self._origrepo.references.get(ref)
  815. self.repo.references.create(
  816. ref, reference.get_object().oid.hex
  817. )
  818. return self
  819. def __exit__(self, exc_type, exc_value, traceback):
  820. """ Exit the context manager, removing the temorary clone. """
  821. shutil.rmtree(self.repopath)
  822. def push(self, username, sbranch, tbranch=None, force=False, **extra):
  823. """ Push the repo back to its origin.
  824. Args:
  825. username (string): The user on who's account this push is
  826. sbranch (string): Source branch to push
  827. tbranch (string): Target branch if different from sbranch
  828. extra (dict): Extra fields passed to the remote side. Either via
  829. environment variables, or as X-Extra-<key> HTTP headers.
  830. """
  831. pushref = "%s:%s" % (sbranch, tbranch if tbranch else sbranch)
  832. if "pull_request" in extra:
  833. extra["pull_request_uid"] = extra["pull_request"].uid
  834. del extra["pull_request"]
  835. if self._project.is_on_repospanner:
  836. regioninfo = pagure_config["REPOSPANNER_REGIONS"][
  837. self._project.repospanner_region
  838. ]
  839. extra.update(
  840. {
  841. "username": username,
  842. "repotype": self._repotype,
  843. "project_name": self._project.name,
  844. "project_user": self._project.user.username
  845. if self._project.is_fork
  846. else "",
  847. "project_namespace": self._project.namespace or "",
  848. }
  849. )
  850. args = []
  851. for opt in extra:
  852. args.extend(["--extra", opt, extra[opt]])
  853. command = [
  854. "git",
  855. "-c",
  856. "protocol.ext.allow=always",
  857. "push",
  858. "ext::%s %s %s"
  859. % (
  860. pagure_config["REPOBRIDGE_BINARY"],
  861. " ".join(args),
  862. self._project._repospanner_repo_name(self._repotype),
  863. ),
  864. "--repo",
  865. self.repopath,
  866. ]
  867. environ = {
  868. "USER": "pagure",
  869. "REPOBRIDGE_CONFIG": ":environment:",
  870. "REPOBRIDGE_BASEURL": regioninfo["url"],
  871. "REPOBRIDGE_CA": regioninfo["ca"],
  872. "REPOBRIDGE_CERT": regioninfo["push_cert"]["cert"],
  873. "REPOBRIDGE_KEY": regioninfo["push_cert"]["key"],
  874. }
  875. else:
  876. command = ["git", "push", "origin"]
  877. if force:
  878. command.append("--force")
  879. environ = {}
  880. try:
  881. _log.debug(
  882. "Running a git push of %s to %s"
  883. % (pushref, self._path or self._project.fullname)
  884. )
  885. env = os.environ.copy()
  886. env["GL_USER"] = username
  887. env["GL_BYPASS_ACCESS_CHECKS"] = "1"
  888. if pagure_config.get("GITOLITE_HOME"):
  889. env["HOME"] = pagure_config["GITOLITE_HOME"]
  890. env.update(environ)
  891. env.update(extra)
  892. out = subprocess.check_output(
  893. command + [pushref],
  894. cwd=self.repopath,
  895. stderr=subprocess.STDOUT,
  896. env=env,
  897. )
  898. _log.debug("Output: %s" % out)
  899. except subprocess.CalledProcessError as ex:
  900. # This should never really happen, since we control the repos, but
  901. # this way, we can be sure to get the output logged
  902. remotes = []
  903. for line in ex.output.decode("utf-8").split("\n"):
  904. _log.info("Remote line: %s", line)
  905. if line.startswith("remote: "):
  906. _log.debug("Remote: %s" % line)
  907. remotes.append(line[len("remote: ") :].strip())
  908. if remotes:
  909. _log.info("Remote rejected with: %s" % remotes)
  910. raise pagure.exceptions.PagurePushDenied(
  911. "Remote hook declined the push: %s" % "\n".join(remotes)
  912. )
  913. else:
  914. # Something else happened, pass the original
  915. _log.exception("Error pushing. Output: %s", ex.output)
  916. raise
  917. def _update_file_in_git(
  918. repo, branch, branchto, filename, content, message, user, email
  919. ):
  920. """ Update a specific file in the specified repository with the content
  921. given and commit the change under the user's name.
  922. :arg repo: the Project object from the database
  923. :arg branch: the branch from which the edit is made
  924. :arg branchto: the name of the branch into which to edit the file
  925. :arg filename: the name of the file to save
  926. :arg content: the new content of the file
  927. :arg message: the message of the git commit
  928. :arg user: the user name, to use in the commit
  929. :arg email: the email of the user, to use in the commit
  930. """
  931. _log.info("Updating file: %s in the repo: %s", filename, repo.path)
  932. with TemporaryClone(repo, "main", "edit_file") as tempclone:
  933. newpath = tempclone.repopath
  934. new_repo = tempclone.repo
  935. new_repo.checkout("refs/heads/%s" % branch)
  936. file_path = os.path.join(newpath, filename)
  937. # Get the current index
  938. index = new_repo.index
  939. # Write down what changed
  940. with open(file_path, "wb") as stream:
  941. stream.write(content.replace("\r", "").encode("utf-8"))
  942. # Retrieve the list of files that changed
  943. diff = new_repo.diff()
  944. files = []
  945. for patch in diff:
  946. files.append(patch.delta.new_file.path)
  947. # Add the changes to the index
  948. added = False
  949. for filename in files:
  950. added = True
  951. index.add(filename)
  952. # If not change, return
  953. if not files and not added:
  954. return
  955. # See if there is a parent to this commit
  956. branch_ref = get_branch_ref(new_repo, branch)
  957. parent = branch_ref.get_object()
  958. # See if we need to create the branch
  959. nbranch_ref = None
  960. if branchto not in new_repo.listall_branches():
  961. nbranch_ref = new_repo.create_branch(branchto, parent)
  962. parents = []
  963. if parent:
  964. parents.append(parent.hex)
  965. # Author/commiter will always be this one
  966. name = user.fullname or user.username
  967. author = _make_signature(name=name, email=email)
  968. # Actually commit
  969. new_repo.create_commit(
  970. nbranch_ref.name if nbranch_ref else branch_ref.name,
  971. author,
  972. author,
  973. message.strip(),
  974. new_repo.index.write_tree(),
  975. parents,
  976. )
  977. index.write()
  978. tempclone.push(
  979. user.username,
  980. nbranch_ref.name if nbranch_ref else branch_ref.name,
  981. branchto,
  982. )
  983. return os.path.join("files", filename)
  984. def read_output(cmd, abspath, input=None, keepends=False, error=False, **kw):
  985. """ Read the output from the given command to run.
  986. cmd:
  987. The command to run, this is a list with each space separated into an
  988. element of the list.
  989. abspath:
  990. The absolute path where the command should be ran.
  991. input:
  992. Whether the command should take input from stdin or not.
  993. (Defaults to False)
  994. keepends:
  995. Whether to strip the newline characters at the end of the standard
  996. output or not.
  997. error:
  998. Whether to return both the standard output and the standard error,
  999. or just the standard output.
  1000. (Defaults to False).
  1001. kw*:
  1002. Any other arguments to be passed onto the subprocess.Popen command,
  1003. such as env, shell, executable...
  1004. """
  1005. if input:
  1006. stdin = subprocess.PIPE
  1007. else:
  1008. stdin = None
  1009. procs = subprocess.Popen(
  1010. cmd,
  1011. stdin=stdin,
  1012. stdout=subprocess.PIPE,
  1013. stderr=subprocess.PIPE,
  1014. cwd=abspath,
  1015. **kw
  1016. )
  1017. retcode = procs.wait()
  1018. (out, err) = procs.communicate(input)
  1019. if not isinstance(out, str):
  1020. out = out.decode("utf-8")
  1021. if not isinstance(err, str):
  1022. err = err.decode("utf-8")
  1023. if retcode:
  1024. print("ERROR: %s =-- %s" % (cmd, retcode))
  1025. print(out)
  1026. print(err)
  1027. if not keepends:
  1028. out = out.rstrip("\n\r")
  1029. if error:
  1030. return (out, err)
  1031. else:
  1032. return out
  1033. def read_git_output(
  1034. args, abspath, input=None, keepends=False, error=False, **kw
  1035. ):
  1036. """Read the output of a Git command."""
  1037. return read_output(
  1038. ["git"] + args,
  1039. abspath,
  1040. input=input,
  1041. keepends=keepends,
  1042. error=error,
  1043. **kw
  1044. )
  1045. def read_git_lines(args, abspath, keepends=False, error=False, **kw):
  1046. """Return the lines output by Git command.
  1047. Return as single lines, with newlines stripped off."""
  1048. if error:
  1049. return read_git_output(
  1050. args, abspath, keepends=keepends, error=error, **kw
  1051. )
  1052. else:
  1053. return read_git_output(
  1054. args, abspath, keepends=keepends, **kw
  1055. ).splitlines(keepends)
  1056. def get_revs_between(oldrev, newrev, abspath, refname, forced=False):
  1057. """ Yield revisions between HEAD and BASE. """
  1058. cmd = ["rev-list", "%s...%s" % (oldrev, newrev)]
  1059. if forced:
  1060. head = get_default_branch(abspath)
  1061. cmd.append("^%s" % head)
  1062. if set(newrev) == set("0"):
  1063. cmd = ["rev-list", "%s" % oldrev]
  1064. elif set(oldrev) == set("0") or set(oldrev) == set("^0"):
  1065. head = get_default_branch(abspath)
  1066. cmd = ["rev-list", "%s" % newrev, "^%s" % head]
  1067. if head in refname:
  1068. cmd = ["rev-list", "%s" % newrev]
  1069. return pagure.lib.git.read_git_lines(cmd, abspath)
  1070. def is_forced_push(oldrev, newrev, abspath):
  1071. """ Returns whether there was a force push between HEAD and BASE.
  1072. Doc: http://stackoverflow.com/a/12258773
  1073. """
  1074. if set(oldrev) == set("0"):
  1075. # This is a push that's creating a new branch => certainly ok
  1076. return False
  1077. # Returns if there was any commits deleted in the changeset
  1078. cmd = ["rev-list", "%s" % oldrev, "^%s" % newrev]
  1079. out = pagure.lib.git.read_git_lines(cmd, abspath)
  1080. return len(out) > 0
  1081. def get_base_revision(torev, fromrev, abspath):
  1082. """ Return the base revision between HEAD and BASE.
  1083. This is useful in case of force-push.
  1084. """
  1085. cmd = ["merge-base", fromrev, torev]
  1086. return pagure.lib.git.read_git_lines(cmd, abspath)
  1087. def get_default_branch(abspath):
  1088. """ Return the default branch of a repo. """
  1089. cmd = ["rev-parse", "--abbrev-ref", "HEAD"]
  1090. out = pagure.lib.git.read_git_lines(cmd, abspath)
  1091. if out:
  1092. return out[0]
  1093. else:
  1094. return "master"
  1095. def get_author(commit, abspath):
  1096. """ Return the name of the person that authored the commit. """
  1097. user = pagure.lib.git.read_git_lines(
  1098. ["log", "-1", '--pretty=format:"%an"', commit], abspath
  1099. )[0].replace('"', "")
  1100. return user
  1101. def get_author_email(commit, abspath):
  1102. """ Return the email of the person that authored the commit. """
  1103. user = pagure.lib.git.read_git_lines(
  1104. ["log", "-1", '--pretty=format:"%ae"', commit], abspath
  1105. )[0].replace('"', "")
  1106. return user
  1107. def get_commit_subject(commit, abspath):
  1108. """ Return the subject of the commit. """
  1109. subject = pagure.lib.git.read_git_lines(
  1110. ["log", "-1", '--pretty=format:"%s"', commit], abspath
  1111. )[0].replace('"', "")
  1112. return subject
  1113. def get_repo_info_from_path(gitdir, hide_notfound=False):
  1114. """ Returns the name, username, namespace and type of a git directory
  1115. This gets computed based on the *_FOLDER's in the config file,
  1116. and as such only works for the central file-based repositories.
  1117. Args:
  1118. gitdir (string): Path of the canonical git repository
  1119. hide_notfound (bool): Whether to return a tuple with None's instead of
  1120. raising an error if the regenerated repo didn't exist.
  1121. Can be used to hide the difference between no project access vs not
  1122. existing when looking up private repos.
  1123. Return: (tuple): Tuple with (repotype, username, namespace, repo)
  1124. Some of these elements may be None if not applicable.
  1125. """
  1126. if not os.path.isabs(gitdir):
  1127. raise ValueError("Tried to locate non-absolute gitdir %s" % gitdir)
  1128. gitdir = os.path.normpath(gitdir)
  1129. types = {
  1130. "main": pagure_config["GIT_FOLDER"],
  1131. "docs": pagure_config["DOCS_FOLDER"],
  1132. "tickets": pagure_config["TICKETS_FOLDER"],
  1133. "requests": pagure_config["REQUESTS_FOLDER"],
  1134. }
  1135. match = None
  1136. matchlen = None
  1137. # First find the longest match in types. This makes sure that even if the
  1138. # non-main repos are in a subdir of main (i.e. repos/ and repos/tickets/),
  1139. # we find the correct type.
  1140. for typename in types:
  1141. if not types[typename]:
  1142. continue
  1143. types[typename] = os.path.abspath(types[typename])
  1144. path = types[typename] + "/"
  1145. if gitdir.startswith(path) and (
  1146. matchlen is None or len(path) > matchlen
  1147. ):
  1148. match = typename
  1149. matchlen = len(path)
  1150. if match is None:
  1151. raise ValueError("Gitdir %s could not be located" % gitdir)
  1152. typepath = types[match]
  1153. guesspath = gitdir[len(typepath) + 1 :]
  1154. if len(guesspath) < 5:
  1155. # At least 4 characters for ".git" is required plus one for project
  1156. # name
  1157. raise ValueError("Invalid gitdir %s located" % gitdir)
  1158. # Just in the case we run on a non-*nix system...
  1159. guesspath = guesspath.replace("\\", "/")
  1160. # Now guesspath should be one of:
  1161. # - reponame.git
  1162. # - namespace/reponame.git
  1163. # - forks/username/reponame.git
  1164. # - forks/username/namespace/reponame.git
  1165. repotype = match
  1166. username = None
  1167. namespace = None
  1168. repo = None
  1169. guesspath, repo = os.path.split(guesspath)
  1170. if not repo.endswith(".git"):
  1171. raise ValueError("Git dir looks to not be a bare repo")
  1172. repo = repo[: -len(".git")]
  1173. if not repo:
  1174. raise ValueError("Gitdir %s seems to not be a bare repo" % gitdir)
  1175. # Split the guesspath up, throwing out any empty strings
  1176. splitguess = [part for part in guesspath.split("/") if part]
  1177. if splitguess and splitguess[0] == "forks":
  1178. if len(splitguess) < 2:
  1179. raise ValueError("Invalid gitdir %s" % gitdir)
  1180. username = splitguess[1]
  1181. splitguess = splitguess[2:]
  1182. if splitguess:
  1183. # At this point, we've cut off the repo name at the end, and any forks/
  1184. # indicators and their usernames are also removed, so remains just the
  1185. # namespace
  1186. namespace = os.path.join(*splitguess)
  1187. # Okay, we think we have everything. Let's make doubly sure the path is
  1188. # correct and exists
  1189. rebuiltpath = os.path.join(
  1190. typepath,
  1191. "forks/" if username else "",
  1192. username if username else "",
  1193. namespace if namespace else "",
  1194. repo + ".git",
  1195. )
  1196. if os.path.normpath(rebuiltpath) != gitdir:
  1197. raise ValueError(
  1198. "Rebuilt %s path not identical to gitdir %s"
  1199. % (rebuiltpath, gitdir)
  1200. )
  1201. if not os.path.exists(rebuiltpath) and not hide_notfound:
  1202. raise ValueError("Splitting gitdir %s failed" % gitdir)
  1203. return (repotype, username, namespace, repo)
  1204. def get_repo_name(abspath):
  1205. """ Return the name of the git repo based on its path.
  1206. """
  1207. _, _, _, name = get_repo_info_from_path(abspath)
  1208. return name
  1209. def get_repo_namespace(abspath, gitfolder=None):
  1210. """ Return the name of the git repo based on its path.
  1211. """
  1212. _, _, namespace, _ = get_repo_info_from_path(abspath)
  1213. return namespace
  1214. def get_username(abspath):
  1215. """ Return the username of the git repo based on its path.
  1216. """
  1217. _, username, _, _ = get_repo_info_from_path(abspath)
  1218. return username
  1219. def get_branch_ref(repo, branchname):
  1220. """ Return the reference to the specified branch or raises an exception.
  1221. """
  1222. location = pygit2.GIT_BRANCH_LOCAL
  1223. if branchname not in repo.listall_branches():
  1224. branchname = "origin/%s" % branchname
  1225. location = pygit2.GIT_BRANCH_REMOTE
  1226. branch_ref = repo.lookup_branch(branchname, location)
  1227. if not branch_ref or not branch_ref.resolve():
  1228. raise pagure.exceptions.PagureException(
  1229. "No refs found for %s" % branchname
  1230. )
  1231. return branch_ref.resolve()
  1232. def merge_pull_request(session, request, username, domerge=True):
  1233. """ Merge the specified pull-request.
  1234. """
  1235. if domerge:
  1236. _log.info("%s asked to merge the pull-request: %s", username, request)
  1237. else:
  1238. _log.info("%s asked to diff the pull-request: %s", username, request)
  1239. repopath = None
  1240. if request.remote:
  1241. # Get the fork
  1242. repopath = pagure.utils.get_remote_repo_path(
  1243. request.remote_git, request.branch_from
  1244. )
  1245. elif request.project_from:
  1246. # Get the fork
  1247. repopath = pagure.utils.get_repo_path(request.project_from)
  1248. fork_obj = None
  1249. if repopath:
  1250. fork_obj = PagureRepo(repopath)
  1251. with TemporaryClone(request.project, "main", "merge_pr") as tempclone:
  1252. new_repo = tempclone.repo
  1253. # Update the start and stop commits in the DB, one last time
  1254. diff_commits = diff_pull_request(
  1255. session, request, fork_obj, new_repo, with_diff=False
  1256. )
  1257. _log.info(" %s commit to merge", len(diff_commits))
  1258. if request.project.settings.get(
  1259. "Enforce_signed-off_commits_in_pull-request", False
  1260. ):
  1261. for commit in diff_commits:
  1262. if "signed-off-by" not in commit.message.lower():
  1263. _log.info(" Missing a required: signed-off-by: Bailing")
  1264. raise pagure.exceptions.PagureException(
  1265. "This repo enforces that all commits are "
  1266. "signed off by their author. "
  1267. )
  1268. if not new_repo.is_empty and not new_repo.head_is_unborn:
  1269. try:
  1270. branch_ref = get_branch_ref(new_repo, request.branch)
  1271. except pagure.exceptions.PagureException:
  1272. branch_ref = None
  1273. if not branch_ref:
  1274. _log.info(" Target branch could not be found")
  1275. raise pagure.exceptions.BranchNotFoundException(
  1276. "Branch %s could not be found in the repo %s"
  1277. % (request.branch, request.project.fullname)
  1278. )
  1279. new_repo.checkout(branch_ref)
  1280. if fork_obj:
  1281. # Check/Get the branch from
  1282. branch = None
  1283. try:
  1284. branch = get_branch_ref(fork_obj, request.branch_from)
  1285. except pagure.exceptions.PagureException:
  1286. pass
  1287. if not branch:
  1288. _log.info(" Branch of origin could not be found")
  1289. raise pagure.exceptions.BranchNotFoundException(
  1290. "Branch %s could not be found in the repo %s"
  1291. % (
  1292. request.branch_from,
  1293. request.project_from.fullname
  1294. if request.project_from
  1295. else request.remote_git,
  1296. )
  1297. )
  1298. # Add the fork as remote repo
  1299. reponame = "%s_%s" % (request.user.user, request.uid)
  1300. _log.info(
  1301. " Adding remote: %s pointing to: %s", reponame, repopath
  1302. )
  1303. remote = new_repo.create_remote(reponame, repopath)
  1304. # Fetch the commits
  1305. remote.fetch()
  1306. # repo_commit = fork_obj[branch.get_object().hex]
  1307. repo_commit = new_repo[branch.get_object().hex]
  1308. # Checkout the correct branch
  1309. if new_repo.is_empty or new_repo.head_is_unborn:
  1310. _log.debug(
  1311. " target repo is empty, so PR can be merged using "
  1312. "fast-forward, reporting it"
  1313. )
  1314. if domerge:
  1315. _log.info(" PR merged using fast-forward")
  1316. if not request.project.settings.get("always_merge", False):
  1317. new_repo.create_branch(request.branch, repo_commit)
  1318. commit = repo_commit.oid.hex
  1319. else:
  1320. tree = new_repo.index.write_tree()
  1321. user_obj = pagure.lib.query.get_user(session, username)
  1322. commitname = user_obj.fullname or user_obj.user
  1323. author = _make_signature(
  1324. commitname, user_obj.default_email
  1325. )
  1326. commit = new_repo.create_commit(
  1327. "refs/heads/%s" % request.branch,
  1328. author,
  1329. author,
  1330. "Merge #%s `%s`" % (request.id, request.title),
  1331. tree,
  1332. [repo_commit.oid.hex],
  1333. )
  1334. _log.info(" New head: %s", commit)
  1335. tempclone.push(
  1336. username,
  1337. request.branch,
  1338. request.branch,
  1339. pull_request=request,
  1340. )
  1341. # Update status
  1342. _log.info(" Closing the PR in the DB")
  1343. pagure.lib.query.close_pull_request(
  1344. session, request, username
  1345. )
  1346. return "Changes merged!"
  1347. else:
  1348. _log.info(
  1349. " PR can be merged using fast-forward, reporting it"
  1350. )
  1351. request.merge_status = "FFORWARD"
  1352. session.commit()
  1353. return "FFORWARD"
  1354. else:
  1355. try:
  1356. ref = new_repo.lookup_reference(
  1357. "refs/pull/%s/head" % request.id
  1358. )
  1359. repo_commit = new_repo[ref.target.hex]
  1360. except KeyError:
  1361. pass
  1362. merge = new_repo.merge(repo_commit.oid)
  1363. _log.debug(" Merge: %s", merge)
  1364. if merge is None:
  1365. mergecode = new_repo.merge_analysis(repo_commit.oid)[0]
  1366. _log.debug(" Mergecode: %s", mergecode)
  1367. # Wait until the last minute then check if the PR was already closed
  1368. # by someone else in the mean while and if so, just bail
  1369. if request.status != "Open":
  1370. _log.info(
  1371. " This pull-request has already been merged or closed by %s "
  1372. "on %s" % (request.closed_by.user, request.closed_at)
  1373. )
  1374. raise pagure.exceptions.PagureException(
  1375. "This pull-request was merged or closed by %s"
  1376. % request.closed_by.user
  1377. )
  1378. if (merge is not None and merge.is_uptodate) or ( # noqa
  1379. merge is None and mergecode & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE
  1380. ):
  1381. if domerge:
  1382. _log.info(" PR up to date, closing it")
  1383. pagure.lib.query.close_pull_request(session, request, username)
  1384. try:
  1385. session.commit()
  1386. except SQLAlchemyError: # pragma: no cover
  1387. session.rollback()
  1388. _log.exception(" Could not merge the PR in the DB")
  1389. raise pagure.exceptions.PagureException(
  1390. "Could not close this pull-request"
  1391. )
  1392. raise pagure.exceptions.PagureException(
  1393. "Nothing to do, changes were already merged"
  1394. )
  1395. else:
  1396. _log.info(" PR up to date, reporting it")
  1397. request.merge_status = "NO_CHANGE"
  1398. session.commit()
  1399. return "NO_CHANGE"
  1400. elif (merge is not None and merge.is_fastforward) or ( # noqa
  1401. merge is None and mergecode & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD
  1402. ):
  1403. if domerge:
  1404. _log.info(" PR merged using fast-forward")
  1405. head = new_repo.lookup_reference("HEAD").get_object()
  1406. if not request.project.settings.get("always_merge", False):
  1407. if merge is not None:
  1408. # This is depending on the pygit2 version
  1409. branch_ref.target = merge.fastforward_oid
  1410. elif merge is None and mergecode is not None:
  1411. branch_ref.set_target(repo_commit.oid.hex)
  1412. commit = repo_commit.oid.hex
  1413. else:
  1414. tree = new_repo.index.write_tree()
  1415. user_obj = pagure.lib.query.get_user(session, username)
  1416. commitname = user_obj.fullname or user_obj.user
  1417. author = _make_signature(
  1418. commitname, user_obj.default_email
  1419. )
  1420. commit_message = "Merge #%s `%s`" % (
  1421. request.id,
  1422. request.title,
  1423. )
  1424. if request.project.settings.get(
  1425. "Enforce_signed-off_commits_in_pull-request", False
  1426. ):
  1427. commit_message += "\n\nSigned-off-by: %s <%s>" % (
  1428. commitname,
  1429. user_obj.default_email,
  1430. )
  1431. commit = new_repo.create_commit(
  1432. "refs/heads/%s" % request.branch,
  1433. author,
  1434. author,
  1435. commit_message,
  1436. tree,
  1437. [head.hex, repo_commit.oid.hex],
  1438. )
  1439. _log.info(" New head: %s", commit)
  1440. tempclone.push(
  1441. username,
  1442. branch_ref.name,
  1443. request.branch,
  1444. pull_request=request,
  1445. )
  1446. else:
  1447. _log.info(
  1448. " PR can be merged using fast-forward, reporting it"
  1449. )
  1450. request.merge_status = "FFORWARD"
  1451. session.commit()
  1452. return "FFORWARD"
  1453. else:
  1454. tree = None
  1455. try:
  1456. tree = new_repo.index.write_tree()
  1457. except pygit2.GitError as err:
  1458. _log.debug(
  1459. " Could not write down the new tree: " "merge conflicts"
  1460. )
  1461. _log.debug(err)
  1462. if domerge:
  1463. _log.info(" Merge conflict: Bailing")
  1464. raise pagure.exceptions.PagureException("Merge conflicts!")
  1465. else:
  1466. _log.info(" Merge conflict, reporting it")
  1467. request.merge_status = "CONFLICTS"
  1468. session.commit()
  1469. return "CONFLICTS"
  1470. if domerge:
  1471. if request.project.settings.get(
  1472. "disable_non_fast-forward_merges", False
  1473. ):
  1474. _log.info(" Merge non-FF PR is disabled for this project")
  1475. return "MERGE"
  1476. _log.info(" Writing down merge commit")
  1477. head = new_repo.lookup_reference("HEAD").get_object()
  1478. _log.info(
  1479. " Basing on: %s - %s", head.hex, repo_commit.oid.hex
  1480. )
  1481. user_obj = pagure.lib.query.get_user(session, username)
  1482. commitname = user_obj.fullname or user_obj.user
  1483. author = _make_signature(commitname, user_obj.default_email)
  1484. commit_message = "Merge #%s `%s`" % (request.id, request.title)
  1485. if request.project.settings.get(
  1486. "Enforce_signed-off_commits_in_pull-request", False
  1487. ):
  1488. commit_message += "\n\nSigned-off-by: %s <%s>" % (
  1489. commitname,
  1490. user_obj.default_email,
  1491. )
  1492. commit = new_repo.create_commit(
  1493. "refs/heads/%s" % request.branch,
  1494. author,
  1495. author,
  1496. commit_message,
  1497. tree,
  1498. [head.hex, repo_commit.oid.hex],
  1499. )
  1500. _log.info(" New head: %s", commit)
  1501. local_ref = "refs/heads/_pagure_topush"
  1502. new_repo.create_reference(local_ref, commit)
  1503. tempclone.push(
  1504. username, local_ref, request.branch, pull_request=request
  1505. )
  1506. else:
  1507. _log.info(
  1508. " PR can be merged with a merge commit, " "reporting it"
  1509. )
  1510. request.merge_status = "MERGE"
  1511. session.commit()
  1512. return "MERGE"
  1513. # Update status
  1514. _log.info(" Closing the PR in the DB")
  1515. pagure.lib.query.close_pull_request(session, request, username)
  1516. return "Changes merged!"
  1517. def rebase_pull_request(request, username):
  1518. """ Rebase the specified pull-request.
  1519. Args:
  1520. session (sqlalchemy): the session to connect to the database with
  1521. request (pagure.lib.model.PullRequest): the database object
  1522. corresponding to the pull-request to rebase
  1523. username (string): the name of the user asking for the pull-request
  1524. to be rebased
  1525. Returns: (string or None): Pull-request rebased
  1526. Raises: pagure.exceptions.PagureException
  1527. """
  1528. _log.info("%s asked to rebased the pull-request: %s", username, request)
  1529. if request.remote:
  1530. # Get the fork
  1531. repopath = pagure.utils.get_remote_repo_path(
  1532. request.remote_git, request.branch_from
  1533. )
  1534. elif request.project_from:
  1535. # Get the fork
  1536. repopath = pagure.utils.get_repo_path(request.project_from)
  1537. else:
  1538. _log.info(
  1539. "PR is neither from a remote git repo or an existing local "
  1540. "repo, bailing"
  1541. )
  1542. return
  1543. if not request.project or not os.path.exists(
  1544. pagure.utils.get_repo_path(request.project)
  1545. ):
  1546. _log.info(
  1547. "Could not find the targeted git repository for %s",
  1548. request.project.fullname,
  1549. )
  1550. raise pagure.exceptions.PagureException(
  1551. "Could not find the targeted git repository for %s"
  1552. % request.project.fullname
  1553. )
  1554. with TemporaryClone(
  1555. project=request.project,
  1556. repotype="main",
  1557. action="rebase_pr",
  1558. path=repopath,
  1559. ) as tempclone:
  1560. new_repo = tempclone.repo
  1561. new_repo.checkout("refs/heads/%s" % request.branch_from)
  1562. # Add the upstream repo as remote
  1563. upstream = "%s_%s" % (request.user.user, request.uid)
  1564. upstream_path = pagure.utils.get_repo_path(request.project)
  1565. _log.info(
  1566. " Adding remote: %s pointing to: %s", upstream, upstream_path
  1567. )
  1568. remote = new_repo.create_remote(upstream, upstream_path)
  1569. # Fetch the commits
  1570. remote.fetch()
  1571. def _run_command(command):
  1572. _log.info("Running command: %s", command)
  1573. try:
  1574. out = subprocess.check_output(
  1575. command, cwd=tempclone.repopath, stderr=subprocess.STDOUT
  1576. )
  1577. _log.info(" command ran successfully")
  1578. _log.debug("Output: %s" % out)
  1579. except subprocess.CalledProcessError as err:
  1580. _log.debug(
  1581. "Rebase FAILED: {cmd} returned code {code} with the "
  1582. "following output: {output}".format(
  1583. cmd=err.cmd, code=err.returncode, output=err.output
  1584. )
  1585. )
  1586. raise pagure.exceptions.PagureException(
  1587. "Did not manage to rebase this pull-request"
  1588. )
  1589. # Configure git for that user
  1590. command = ["git", "config", "user.name", username]
  1591. _run_command(command)
  1592. command = ["git", "config", "user.email", "%s@pagure" % username]
  1593. _run_command(command)
  1594. # Do the rebase
  1595. command = ["git", "pull", "--rebase", upstream, request.branch]
  1596. _run_command(command)
  1597. # Retrieve the reference of the branch we're working on
  1598. try:
  1599. branch_ref = get_branch_ref(new_repo, request.branch_from)
  1600. except pagure.exceptions.PagureException:
  1601. branch_ref = None
  1602. if not branch_ref:
  1603. _log.debug(" Target branch could not be found")
  1604. raise pagure.exceptions.BranchNotFoundException(
  1605. "Branch %s could not be found in the repo %s"
  1606. % (request.branch, request.project.fullname)
  1607. )
  1608. # Push the changes
  1609. _log.info("Pushing %s to %s", branch_ref.name, request.branch_from)
  1610. try:
  1611. tempclone.push(
  1612. username,
  1613. branch_ref.name,
  1614. request.branch_from,
  1615. pull_request=request,
  1616. force=True,
  1617. )
  1618. except subprocess.CalledProcessError as err:
  1619. _log.debug(
  1620. "Rebase FAILED: {cmd} returned code {code} with the "
  1621. "following output: {output}".format(
  1622. cmd=err.cmd, code=err.returncode, output=err.output
  1623. )
  1624. )
  1625. raise pagure.exceptions.PagureException(
  1626. "Did not manage to rebase this pull-request"
  1627. )
  1628. return "Pull-request rebased"
  1629. def get_diff_info(repo_obj, orig_repo, branch_from, branch_to, prid=None):
  1630. """ Return the info needed to see a diff or make a Pull-Request between
  1631. the two specified repo.
  1632. :arg repo_obj: The pygit2.Repository object of the first git repo
  1633. :arg orig_repo: The pygit2.Repository object of the second git repo
  1634. :arg branch_from: the name of the branch having the changes, in the
  1635. first git repo
  1636. :arg branch_to: the name of the branch in which we want to merge the
  1637. changes in the second git repo
  1638. :kwarg prid: the identifier of the pull-request to
  1639. """
  1640. try:
  1641. frombranch = repo_obj.lookup_branch(branch_from)
  1642. except ValueError:
  1643. raise pagure.exceptions.BranchNotFoundException(
  1644. "Branch %s does not exist" % branch_from
  1645. )
  1646. except AttributeError:
  1647. frombranch = None
  1648. if not frombranch and prid is None and repo_obj and not repo_obj.is_empty:
  1649. raise pagure.exceptions.BranchNotFoundException(
  1650. "Branch %s does not exist" % branch_from
  1651. )
  1652. branch = None
  1653. if branch_to:
  1654. try:
  1655. branch = orig_repo.lookup_branch(branch_to)
  1656. except ValueError:
  1657. raise pagure.exceptions.BranchNotFoundException(
  1658. "Branch %s does not exist" % branch_to
  1659. )
  1660. local_branches = orig_repo.listall_branches(pygit2.GIT_BRANCH_LOCAL)
  1661. if not branch and local_branches:
  1662. raise pagure.exceptions.BranchNotFoundException(
  1663. "Branch %s could not be found in the target repo" % branch_to
  1664. )
  1665. commitid = None
  1666. if frombranch:
  1667. commitid = frombranch.get_object().hex
  1668. elif prid is not None:
  1669. # If there is not branch found but there is a PR open, use the ref
  1670. # of that PR in the main repo
  1671. try:
  1672. ref = orig_repo.lookup_reference("refs/pull/%s/head" % prid)
  1673. commitid = ref.target.hex
  1674. except KeyError:
  1675. pass
  1676. if not commitid and repo_obj and not repo_obj.is_empty:
  1677. raise pagure.exceptions.PagureException(
  1678. "No branch from which to pull or local PR reference were found"
  1679. )
  1680. diff_commits = []
  1681. diff = None
  1682. orig_commit = None
  1683. # If the fork is empty but there is a PR open, use the main repo
  1684. if (not repo_obj or repo_obj.is_empty) and prid is not None:
  1685. repo_obj = orig_repo
  1686. if not repo_obj.is_empty and not orig_repo.is_empty:
  1687. _log.info(
  1688. "pagure.lib.git.get_diff_info: Pulling into a non-empty repo"
  1689. )
  1690. if branch:
  1691. orig_commit = orig_repo[branch.get_object().hex]
  1692. main_walker = orig_repo.walk(
  1693. orig_commit.oid.hex, pygit2.GIT_SORT_NONE
  1694. )
  1695. repo_commit = repo_obj[commitid]
  1696. branch_walker = repo_obj.walk(
  1697. repo_commit.oid.hex, pygit2.GIT_SORT_NONE
  1698. )
  1699. main_commits = set()
  1700. branch_commits = set()
  1701. while 1:
  1702. com = None
  1703. if branch:
  1704. try:
  1705. com = next(main_walker)
  1706. main_commits.add(com.oid.hex)
  1707. except StopIteration:
  1708. com = None
  1709. try:
  1710. branch_commit = next(branch_walker)
  1711. except StopIteration:
  1712. branch_commit = None
  1713. # We sure never end up here but better safe than sorry
  1714. if com is None and branch_commit is None:
  1715. break
  1716. if branch_commit:
  1717. branch_commits.add(branch_commit.oid.hex)
  1718. diff_commits.append(branch_commit)
  1719. if main_commits.intersection(branch_commits):
  1720. break
  1721. # If master is ahead of branch, we need to remove the commits
  1722. # that are after the first one found in master
  1723. i = 0
  1724. if diff_commits and main_commits:
  1725. for i in range(len(diff_commits)):
  1726. if diff_commits[i].oid.hex in main_commits:
  1727. break
  1728. diff_commits = diff_commits[:i]
  1729. _log.debug("Diff commits: %s", diff_commits)
  1730. if diff_commits:
  1731. first_commit = repo_obj[diff_commits[-1].oid.hex]
  1732. if len(first_commit.parents) > 0:
  1733. diff = repo_obj.diff(
  1734. repo_obj.revparse_single(first_commit.parents[0].oid.hex),
  1735. repo_obj.revparse_single(diff_commits[0].oid.hex),
  1736. )
  1737. elif first_commit.oid.hex == diff_commits[0].oid.hex:
  1738. _log.info(
  1739. "pagure.lib.git.get_diff_info: First commit is also the "
  1740. "last commit"
  1741. )
  1742. diff = diff_commits[0].tree.diff_to_tree(swap=True)
  1743. elif orig_repo.is_empty and repo_obj and not repo_obj.is_empty:
  1744. _log.info("pagure.lib.git.get_diff_info: Pulling into an empty repo")
  1745. if "master" in repo_obj.listall_branches():
  1746. repo_commit = repo_obj[repo_obj.head.target]
  1747. else:
  1748. branch = repo_obj.lookup_branch(branch_from)
  1749. repo_commit = branch.get_object()
  1750. for commit in repo_obj.walk(repo_commit.oid.hex, pygit2.GIT_SORT_NONE):
  1751. diff_commits.append(commit)
  1752. _log.debug("Diff commits: %s", diff_commits)
  1753. diff = repo_commit.tree.diff_to_tree(swap=True)
  1754. else:
  1755. raise pagure.exceptions.PagureException(
  1756. "Fork is empty, there are no commits to create a pull "
  1757. "request with"
  1758. )
  1759. _log.info(
  1760. "pagure.lib.git.get_diff_info: diff_commits length: %s",
  1761. len(diff_commits),
  1762. )
  1763. _log.info("pagure.lib.git.get_diff_info: original commit: %s", orig_commit)
  1764. return (diff, diff_commits, orig_commit)
  1765. def diff_pull_request(
  1766. session, request, repo_obj, orig_repo, with_diff=True, notify=True
  1767. ):
  1768. """ Returns the diff and the list of commits between the two git repos
  1769. mentionned in the given pull-request.
  1770. :arg session: The sqlalchemy session to connect to the database
  1771. :arg request: The pagure.lib.model.PullRequest object of the pull-request
  1772. to look into
  1773. :arg repo_obj: The pygit2.Repository object of the first git repo
  1774. :arg orig_repo: The pygit2.Repository object of the second git repo
  1775. :arg with_diff: A boolean on whether to return the diff with the list
  1776. of commits (or just the list of commits)
  1777. """
  1778. diff = None
  1779. diff_commits = []
  1780. diff, diff_commits, _ = get_diff_info(
  1781. repo_obj,
  1782. orig_repo,
  1783. request.branch_from,
  1784. request.branch,
  1785. prid=request.id,
  1786. )
  1787. if request.status == "Open" and diff_commits:
  1788. first_commit = diff_commits[-1]
  1789. # Check if we can still rely on the merge_status
  1790. commenttext = None
  1791. if (
  1792. request.commit_start != first_commit.oid.hex
  1793. or request.commit_stop != diff_commits[0].oid.hex
  1794. ):
  1795. request.merge_status = None
  1796. if request.commit_start:
  1797. pr_action = "updated"
  1798. new_commits_count = 0
  1799. commenttext = ""
  1800. for i in diff_commits:
  1801. if i.oid.hex == request.commit_stop:
  1802. break
  1803. new_commits_count = new_commits_count + 1
  1804. commenttext = "%s * ``%s``\n" % (
  1805. commenttext,
  1806. i.message.strip().split("\n")[0],
  1807. )
  1808. if new_commits_count == 1:
  1809. commenttext = "**%d new commit added**\n\n%s" % (
  1810. new_commits_count,
  1811. commenttext,
  1812. )
  1813. else:
  1814. commenttext = "**%d new commits added**\n\n%s" % (
  1815. new_commits_count,
  1816. commenttext,
  1817. )
  1818. if (
  1819. request.commit_start
  1820. and request.commit_start != first_commit.oid.hex
  1821. ):
  1822. pr_action = "rebased"
  1823. commenttext = "rebased onto %s" % first_commit.oid.hex
  1824. request.commit_start = first_commit.oid.hex
  1825. request.commit_stop = diff_commits[0].oid.hex
  1826. session.add(request)
  1827. session.commit()
  1828. tasks.sync_pull_ref.delay(
  1829. request.project.name,
  1830. request.project.namespace,
  1831. request.project.user.username if request.project.is_fork else None,
  1832. request.id,
  1833. )
  1834. if commenttext:
  1835. tasks.link_pr_to_ticket.delay(request.uid)
  1836. if notify:
  1837. if pr_action:
  1838. pagure.lib.notify.log(
  1839. request.project,
  1840. topic="pull-request.%s" % pr_action,
  1841. msg=dict(
  1842. pullrequest=request.to_json(
  1843. with_comments=False, public=True
  1844. ),
  1845. agent="pagure",
  1846. ),
  1847. )
  1848. pagure.lib.query.add_pull_request_comment(
  1849. session,
  1850. request,
  1851. commit=None,
  1852. tree_id=None,
  1853. filename=None,
  1854. row=None,
  1855. comment="%s" % commenttext,
  1856. user=request.user.username,
  1857. notify=False,
  1858. notification=True,
  1859. )
  1860. session.commit()
  1861. else:
  1862. pagure.lib.git.update_git(request, repo=request.project)
  1863. if with_diff:
  1864. return (diff_commits, diff)
  1865. else:
  1866. return diff_commits
  1867. def update_pull_ref(request, repo):
  1868. """ Create or update the refs/pull/ reference in the git repo.
  1869. """
  1870. repopath = pagure.utils.get_repo_path(request.project)
  1871. reponame = "%s_%s" % (request.user.user, request.uid)
  1872. _log.info(" Adding remote: %s pointing to: %s", reponame, repopath)
  1873. rc = RemoteCollection(repo)
  1874. try:
  1875. # we do rc.delete(reponame) both here and in the finally block below:
  1876. # * here: it's useful for cases when worker was interrupted
  1877. # on the previous execution of this function and didn't manage
  1878. # to remove the ref
  1879. # * in the finally clause: to remove the ref so that it doesn't stay
  1880. # in the fork forever (as noted above, it might still stay there
  1881. # if the worker gets interrupted, but that's not a huge deal)
  1882. rc[reponame]
  1883. rc.delete(reponame)
  1884. except KeyError:
  1885. pass
  1886. remote = rc.create(reponame, repopath)
  1887. try:
  1888. _log.info(
  1889. " Pushing refs/heads/%s to refs/pull/%s/head",
  1890. request.branch_from,
  1891. request.id,
  1892. )
  1893. refname = "+refs/heads/%s:refs/pull/%s/head" % (
  1894. request.branch_from,
  1895. request.id,
  1896. )
  1897. PagureRepo.push(remote, refname)
  1898. finally:
  1899. rc.delete(reponame)
  1900. def get_git_tags(project, with_commits=False):
  1901. """ Returns the list of tags created in the git repositorie of the
  1902. specified project.
  1903. """
  1904. repopath = pagure.utils.get_repo_path(project)
  1905. repo_obj = PagureRepo(repopath)
  1906. if with_commits:
  1907. tags = {}
  1908. for tag in repo_obj.listall_references():
  1909. if tag.startswith("refs/tags/"):
  1910. ref = repo_obj.lookup_reference(tag)
  1911. if ref:
  1912. com = ref.get_object()
  1913. if com:
  1914. tags[tag.split("refs/tags/")[1]] = com.oid.hex
  1915. else:
  1916. tags = [
  1917. tag.split("refs/tags/")[1]
  1918. for tag in repo_obj.listall_references()
  1919. if tag.startswith("refs/tags/")
  1920. ]
  1921. return tags
  1922. def get_git_tags_objects(project):
  1923. """ Returns the list of references of the tags created in the git
  1924. repositorie the specified project.
  1925. The list is sorted using the time of the commit associated to the tag """
  1926. repopath = pagure.utils.get_repo_path(project)
  1927. repo_obj = PagureRepo(repopath)
  1928. tags = {}
  1929. for tag in repo_obj.listall_references():
  1930. if "refs/tags/" in tag and repo_obj.lookup_reference(tag):
  1931. commit_time = None
  1932. try:
  1933. theobject = repo_obj[repo_obj.lookup_reference(tag).target]
  1934. except ValueError:
  1935. theobject = None
  1936. objecttype = ""
  1937. if isinstance(theobject, pygit2.Tag):
  1938. underlying_obj = theobject.get_object()
  1939. if isinstance(underlying_obj, pygit2.Tree):
  1940. continue
  1941. commit_time = underlying_obj.commit_time
  1942. objecttype = "tag"
  1943. elif isinstance(theobject, pygit2.Commit):
  1944. commit_time = theobject.commit_time
  1945. objecttype = "commit"
  1946. tags[commit_time] = {
  1947. "object": theobject,
  1948. "tagname": tag.replace("refs/tags/", ""),
  1949. "date": commit_time,
  1950. "objecttype": objecttype,
  1951. "head_msg": None,
  1952. "body_msg": None,
  1953. }
  1954. if objecttype == "tag":
  1955. head_msg, _, body_msg = tags[commit_time][
  1956. "object"
  1957. ].message.partition("\n")
  1958. if body_msg.strip().endswith("\n-----END PGP SIGNATURE-----"):
  1959. body_msg = body_msg.rsplit(
  1960. "-----BEGIN PGP SIGNATURE-----", 1
  1961. )[0].strip()
  1962. tags[commit_time]["head_msg"] = head_msg
  1963. tags[commit_time]["body_msg"] = body_msg
  1964. sorted_tags = []
  1965. for tag in sorted(tags, reverse=True):
  1966. sorted_tags.append(tags[tag])
  1967. return sorted_tags
  1968. def log_commits_to_db(session, project, commits, gitdir):
  1969. """ Log the given commits to the DB. """
  1970. repo_obj = PagureRepo(gitdir)
  1971. for commitid in commits:
  1972. try:
  1973. commit = repo_obj[commitid]
  1974. except ValueError:
  1975. continue
  1976. try:
  1977. author_obj = pagure.lib.query.get_user(
  1978. session, commit.author.email
  1979. )
  1980. except pagure.exceptions.PagureException:
  1981. author_obj = None
  1982. date_created = arrow.get(commit.commit_time)
  1983. log = model.PagureLog(
  1984. user_id=author_obj.id if author_obj else None,
  1985. user_email=commit.author.email if not author_obj else None,
  1986. project_id=project.id,
  1987. log_type="committed",
  1988. ref_id=commit.oid.hex,
  1989. date=date_created.date(),
  1990. date_created=date_created.datetime,
  1991. )
  1992. session.add(log)
  1993. def reinit_git(project, repofolder):
  1994. """ Delete and recreate a git folder
  1995. :args project: SQLAlchemy object of the project
  1996. :args folder: The folder which contains the git repos
  1997. like TICKETS_FOLDER for tickets and REQUESTS_FOLDER for
  1998. pull requests
  1999. """
  2000. repo_path = os.path.join(repofolder, project.path)
  2001. if not os.path.exists(repo_path):
  2002. return
  2003. # delete that repo
  2004. shutil.rmtree(repo_path)
  2005. # create it again
  2006. pygit2.init_repository(
  2007. repo_path, bare=True, mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP
  2008. )
  2009. def get_git_branches(project):
  2010. """ Return a list of branches for the project
  2011. :arg project: The Project instance to get the branches for
  2012. """
  2013. repo_path = pagure.utils.get_repo_path(project)
  2014. repo_obj = pygit2.Repository(repo_path)
  2015. return repo_obj.listall_branches()
  2016. def new_git_branch(
  2017. username, project, branch, from_branch=None, from_commit=None
  2018. ):
  2019. """ Create a new git branch on the project
  2020. :arg project: The Project instance to get the branches for
  2021. :arg from_branch: The branch to branch off of
  2022. """
  2023. with TemporaryClone(project, "main", "new_branch") as tempclone:
  2024. repo_obj = tempclone.repo
  2025. if not from_branch and not from_commit:
  2026. from_branch = "master"
  2027. branches = repo_obj.listall_branches()
  2028. if from_branch:
  2029. if from_branch not in branches:
  2030. raise pagure.exceptions.PagureException(
  2031. 'The "{0}" branch does not exist'.format(from_branch)
  2032. )
  2033. parent = get_branch_ref(repo_obj, from_branch).get_object()
  2034. else:
  2035. if from_commit not in repo_obj:
  2036. raise pagure.exceptions.PagureException(
  2037. 'The commit "{0}" does not exist'.format(from_commit)
  2038. )
  2039. parent = repo_obj[from_commit]
  2040. if branch not in branches:
  2041. repo_obj.create_branch(branch, parent)
  2042. else:
  2043. raise pagure.exceptions.PagureException(
  2044. 'The branch "{0}" already exists'.format(branch)
  2045. )
  2046. tempclone.push(username, branch, branch)
  2047. def delete_project_repos(project):
  2048. """ Deletes the actual git repositories on disk or repoSpanner
  2049. Args:
  2050. project (Project): Project to delete repos for
  2051. """
  2052. for repotype in pagure.lib.query.get_repotypes():
  2053. if project.is_on_repospanner:
  2054. _, regioninfo = project.repospanner_repo_info(repotype)
  2055. _log.debug("Deleting repotype %s", repotype)
  2056. data = {
  2057. "Reponame": project._repospanner_repo_name(
  2058. repotype, project.repospanner_region
  2059. )
  2060. }
  2061. resp = requests.post(
  2062. "%s/admin/deleterepo" % regioninfo["url"],
  2063. json=data,
  2064. verify=regioninfo["ca"],
  2065. cert=(
  2066. regioninfo["admin_cert"]["cert"],
  2067. regioninfo["admin_cert"]["key"],
  2068. ),
  2069. )
  2070. resp.raise_for_status()
  2071. resp = resp.json()
  2072. _log.debug("Response json: %s", resp)
  2073. if not resp["Success"]:
  2074. raise Exception(
  2075. "Error in repoSpanner API call: %s" % resp["Error"]
  2076. )
  2077. else:
  2078. repopath = project.repopath(repotype)
  2079. if repopath is None:
  2080. continue
  2081. try:
  2082. shutil.rmtree(repopath)
  2083. except Exception:
  2084. _log.exception(
  2085. "Failed to remove repotype %s for %s",
  2086. repotype,
  2087. project.fullname,
  2088. )
  2089. def set_up_project_hooks(project, region, hook=None):
  2090. """ Makes sure the git repositories for a project have their hooks setup.
  2091. Args:
  2092. project (model.Project): Project to set up hooks for
  2093. region (string or None): repoSpanner region to set hooks up for
  2094. hook (string): The hook ID to set up in repoSpanner (tests only)
  2095. """
  2096. if region is None:
  2097. # This repo is not on repoSpanner, create hooks locally
  2098. pagure.hooks.BaseHook.set_up(project)
  2099. else:
  2100. regioninfo = pagure_config["REPOSPANNER_REGIONS"].get(region)
  2101. if not regioninfo:
  2102. raise ValueError(
  2103. "Invalid repoSpanner region %s looked up" % region
  2104. )
  2105. if not hook:
  2106. hook = regioninfo["hook"]
  2107. if not hook:
  2108. # No hooks to set up for this region
  2109. return
  2110. for repotype in pagure.lib.query.get_repotypes():
  2111. data = {
  2112. "Reponame": project._repospanner_repo_name(repotype, region),
  2113. "UpdateRequest": {
  2114. "hook-prereceive": hook,
  2115. "hook-update": hook,
  2116. "hook-postreceive": hook,
  2117. },
  2118. }
  2119. resp = requests.post(
  2120. "%s/admin/editrepo" % regioninfo["url"],
  2121. json=data,
  2122. verify=regioninfo["ca"],
  2123. cert=(
  2124. regioninfo["admin_cert"]["cert"],
  2125. regioninfo["admin_cert"]["key"],
  2126. ),
  2127. )
  2128. resp.raise_for_status()
  2129. resp = resp.json()
  2130. _log.debug("Response json: %s", resp)
  2131. if not resp["Success"]:
  2132. raise Exception(
  2133. "Error in repoSpanner API call: %s" % resp["Error"]
  2134. )
  2135. def _create_project_repo(project, region, templ, ignore_existing, repotype):
  2136. """ Creates a single specific git repository on disk or repoSpanner
  2137. Args:
  2138. project (Project): Project to create repos for
  2139. region (string or None): repoSpanner region to create the repos in
  2140. templ (string): Template directory, only valid for non-repoSpanner
  2141. ignore_existing (bool): Whether a repo already existing is fatal
  2142. repotype (string): Repotype to create
  2143. Returns: (string or None): Directory created
  2144. """
  2145. if region:
  2146. # repoSpanner creation
  2147. regioninfo = pagure_config["REPOSPANNER_REGIONS"][region]
  2148. # First create the repository on the repoSpanner region
  2149. _log.debug("Creating repotype %s", repotype)
  2150. data = {
  2151. "Reponame": project._repospanner_repo_name(repotype, region),
  2152. "Public": False,
  2153. }
  2154. resp = requests.post(
  2155. "%s/admin/createrepo" % regioninfo["url"],
  2156. json=data,
  2157. verify=regioninfo["ca"],
  2158. cert=(
  2159. regioninfo["admin_cert"]["cert"],
  2160. regioninfo["admin_cert"]["key"],
  2161. ),
  2162. )
  2163. resp.raise_for_status()
  2164. resp = resp.json()
  2165. _log.debug("Response json: %s", resp)
  2166. if not resp["Success"]:
  2167. if "already exists" in resp["Error"]:
  2168. if ignore_existing:
  2169. return None
  2170. else:
  2171. raise pagure.exceptions.RepoExistsException(resp["Error"])
  2172. raise Exception(
  2173. "Error in repoSpanner API call: %s" % resp["Error"]
  2174. )
  2175. return None
  2176. else:
  2177. # local repo
  2178. repodir = project.repopath(repotype)
  2179. if repodir is None:
  2180. # This repo type is disabled
  2181. return None
  2182. if os.path.exists(repodir):
  2183. if not ignore_existing:
  2184. raise pagure.exceptions.RepoExistsException(
  2185. "The %s repo %s already exists" % (repotype, project.path)
  2186. )
  2187. else:
  2188. return None
  2189. if repotype == "main":
  2190. pygit2.init_repository(repodir, bare=True, template_path=templ)
  2191. if not project.private:
  2192. # Make the repo exportable via apache
  2193. http_clone_file = os.path.join(repodir, "git-daemon-export-ok")
  2194. if not os.path.exists(http_clone_file):
  2195. with open(http_clone_file, "w"):
  2196. pass
  2197. else:
  2198. pygit2.init_repository(
  2199. repodir,
  2200. bare=True,
  2201. mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP,
  2202. )
  2203. return repodir
  2204. def create_project_repos(project, region, templ, ignore_existing):
  2205. """ Creates the actual git repositories on disk or repoSpanner
  2206. Args:
  2207. project (Project): Project to create repos for
  2208. region (string or None): repoSpanner region to create the repos in
  2209. templ (string): Template directory, only valid for non-repoSpanner
  2210. ignore_existing (bool): Whether a repo already existing is fatal
  2211. """
  2212. if region and templ:
  2213. raise Exception("repoSpanner is incompatible with template directory")
  2214. created_dirs = []
  2215. try:
  2216. for repotype in pagure.lib.query.get_repotypes():
  2217. created = _create_project_repo(
  2218. project, region, templ, ignore_existing, repotype
  2219. )
  2220. if created:
  2221. created_dirs.append(created)
  2222. except Exception:
  2223. for created in created_dirs:
  2224. shutil.rmtree(created)
  2225. raise
  2226. set_up_project_hooks(project, region)
  2227. def get_stats_patch(patch):
  2228. """ Returns some statistics about a given patch.
  2229. These stats include:
  2230. status: if the file was added (A), deleted (D), modified (M) or
  2231. renamed (R)
  2232. old_path: the path to the old file
  2233. new_path: the path to the new file
  2234. lines_added: the number of lines added in this patch
  2235. lines_removed: the number of lines removed in this patch
  2236. All these information are returned in a dict.
  2237. Args:
  2238. patch (pygit2.Patch): the patch object to get stats on
  2239. Returns: a dict with the stats described above
  2240. Raises (pagure.exceptions.PagureException): if for some reason (likely
  2241. a change in pygit2's API) this function does not manage to gather
  2242. all the stats it should
  2243. """
  2244. output = {
  2245. "lines_added": patch.line_stats[1],
  2246. "lines_removed": patch.line_stats[2],
  2247. "new_path": None,
  2248. "old_path": None,
  2249. "status": None,
  2250. "new_id": None,
  2251. "old_id": None,
  2252. }
  2253. if hasattr(patch, "new_file_path"):
  2254. # Older pygit2
  2255. status = patch.status
  2256. if patch.new_file_path != patch.old_file_path:
  2257. status = "R"
  2258. output["status"] = status
  2259. output["new_path"] = patch.new_file_path
  2260. output["old_path"] = patch.old_file_path
  2261. output["new_id"] = str(patch.new_id)
  2262. output["old_id"] = str(patch.old_id)
  2263. elif hasattr(patch, "delta"):
  2264. # Newer pygit2
  2265. if patch.delta.new_file.mode == 0 and patch.delta.old_file.mode in [
  2266. 33188,
  2267. 33261,
  2268. ]:
  2269. status = "D"
  2270. elif (
  2271. patch.delta.new_file.mode in [33188, 33261]
  2272. and patch.delta.old_file.mode == 0
  2273. ):
  2274. status = "A"
  2275. elif patch.delta.new_file.mode in [
  2276. 33188,
  2277. 33261,
  2278. ] and patch.delta.old_file.mode in [33188, 33261]:
  2279. status = "M"
  2280. if patch.delta.new_file.path != patch.delta.old_file.path:
  2281. status = "R"
  2282. output["status"] = status
  2283. output["new_path"] = patch.delta.new_file.path
  2284. output["new_id"] = str(patch.delta.new_file.id)
  2285. output["old_path"] = patch.delta.old_file.path
  2286. output["old_id"] = str(patch.delta.old_file.id)
  2287. if None in output.values(): # pragma: no-cover
  2288. raise pagure.exceptions.PagureException(
  2289. "Unable to properly retrieve the stats for this patch"
  2290. )
  2291. return output
  2292. def generate_archive(project, commit, tag, name, archive_fmt):
  2293. """ Generate the desired archive of the specified project for the
  2294. specified commit with the given name and archive format.
  2295. Args:
  2296. project (pagure.lib.model.Project): the project's repository from
  2297. which to generate the archive
  2298. commit (str): the commit hash to generate the archive of
  2299. name (str): the name to give to the archive
  2300. archive_fmt (str): the format of the archive to generate, can be
  2301. either gzip or tag or tar.gz
  2302. Returns: None
  2303. Raises (pagure.exceptions.PagureException): if an un-supported archive
  2304. format is specified
  2305. """
  2306. def _exclude_git(filename):
  2307. return ".git" in filename
  2308. with TemporaryClone(project, "main", "archive", parent=name) as tempclone:
  2309. repo_obj = tempclone.repo
  2310. commit_obj = repo_obj[commit]
  2311. repo_obj.checkout_tree(commit_obj.tree)
  2312. archive_folder = pagure_config.get("ARCHIVE_FOLDER")
  2313. tag_path = ""
  2314. if tag:
  2315. tag_path = os.path.join("tags", tag)
  2316. target_path = os.path.join(
  2317. archive_folder, project.fullname, tag_path, commit
  2318. )
  2319. if not os.path.exists(target_path):
  2320. os.makedirs(target_path)
  2321. fullpath = os.path.join(target_path, name)
  2322. if archive_fmt == "tar":
  2323. with tarfile.open(name=fullpath + ".tar", mode="w") as tar:
  2324. tar.add(
  2325. name=tempclone.repopath, exclude=_exclude_git, arcname=name
  2326. )
  2327. elif archive_fmt == "tar.gz":
  2328. with tarfile.open(name=fullpath + ".tar.gz", mode="w:gz") as tar:
  2329. tar.add(
  2330. name=tempclone.repopath, exclude=_exclude_git, arcname=name
  2331. )
  2332. elif archive_fmt == "zip":
  2333. # Code from /usr/lib64/python2.7/zipfile.py adjusted for our
  2334. # needs
  2335. def addToZip(zf, path, zippath):
  2336. if _exclude_git(path):
  2337. return
  2338. if os.path.isfile(path):
  2339. zf.write(path, zippath, zipfile.ZIP_DEFLATED)
  2340. elif os.path.isdir(path):
  2341. if zippath:
  2342. zf.write(path, zippath)
  2343. for nm in os.listdir(path):
  2344. if _exclude_git(path):
  2345. continue
  2346. addToZip(
  2347. zf,
  2348. os.path.join(path, nm),
  2349. os.path.join(zippath, nm),
  2350. )
  2351. with zipfile.ZipFile(fullpath + ".zip", "w") as zipstream:
  2352. addToZip(zipstream, tempclone.repopath, name)
  2353. else:
  2354. raise pagure.exceptions.PagureException(
  2355. "Un-support archive format requested: %s", archive_fmt
  2356. )
  2357. def mirror_pull_project(session, project, debug=False):
  2358. """ Mirror locally a project from a remote URL. """
  2359. remote = project.mirrored_from
  2360. repopath = tempfile.mkdtemp(prefix="pagure-mirror_in-")
  2361. lclrepopath = pagure.utils.get_repo_path(project)
  2362. def _run_command(command, logs):
  2363. _log.info("Running the command: %s" % command)
  2364. if debug:
  2365. print("Running the command: %s" % command)
  2366. print(" Running in: %s" % repopath)
  2367. (stdout, stderr) = pagure.lib.git.read_git_lines(
  2368. command, abspath=repopath, error=True
  2369. )
  2370. log = "Output from %s:\n stdout: %s\n stderr: %s" % (
  2371. command,
  2372. stdout,
  2373. stderr,
  2374. )
  2375. logs.append(log)
  2376. if debug:
  2377. print(log)
  2378. return logs
  2379. try:
  2380. # Pull
  2381. logs = []
  2382. logs = _run_command(["clone", "--mirror", remote, "."], logs)
  2383. logs = _run_command(["remote", "add", "local", lclrepopath], logs)
  2384. # Push the changes
  2385. _log.info("Pushing")
  2386. if debug:
  2387. print("Pushing to the local git repo")
  2388. extra = {}
  2389. if project.is_on_repospanner:
  2390. regioninfo = pagure_config["REPOSPANNER_REGIONS"][
  2391. project.repospanner_region
  2392. ]
  2393. extra.update(
  2394. {
  2395. "username": "pagure",
  2396. "repotype": "main",
  2397. "project_name": project.name,
  2398. "project_user": project.user.username
  2399. if project.is_fork
  2400. else "",
  2401. "project_namespace": project.namespace or "",
  2402. }
  2403. )
  2404. args = []
  2405. for opt in extra:
  2406. args.extend(["--extra", opt, extra[opt]])
  2407. command = [
  2408. "git",
  2409. "-c",
  2410. "protocol.ext.allow=always",
  2411. "push",
  2412. "ext::%s %s %s"
  2413. % (
  2414. pagure_config["REPOBRIDGE_BINARY"],
  2415. " ".join(args),
  2416. project._repospanner_repo_name("main"),
  2417. ),
  2418. "--repo",
  2419. repopath,
  2420. ]
  2421. environ = {
  2422. "USER": "pagure",
  2423. "REPOBRIDGE_CONFIG": ":environment:",
  2424. "REPOBRIDGE_BASEURL": regioninfo["url"],
  2425. "REPOBRIDGE_CA": regioninfo["ca"],
  2426. "REPOBRIDGE_CERT": regioninfo["push_cert"]["cert"],
  2427. "REPOBRIDGE_KEY": regioninfo["push_cert"]["key"],
  2428. }
  2429. else:
  2430. command = ["git", "push", "local", "--mirror"]
  2431. environ = {}
  2432. _log.debug("Running a git push to %s", project.fullname)
  2433. env = os.environ.copy()
  2434. env["GL_USER"] = "pagure"
  2435. env["GL_BYPASS_ACCESS_CHECKS"] = "1"
  2436. if pagure_config.get("GITOLITE_HOME"):
  2437. env["HOME"] = pagure_config["GITOLITE_HOME"]
  2438. env.update(environ)
  2439. env.update(extra)
  2440. out = subprocess.check_output(
  2441. command, cwd=repopath, stderr=subprocess.STDOUT, env=env
  2442. )
  2443. log = "Output from %s:" % command
  2444. logs.append(log)
  2445. logs.append(out)
  2446. _log.debug("Output: %s" % out)
  2447. project.mirrored_from_last_log = "\n".join(logs)
  2448. session.add(project)
  2449. session.commit()
  2450. _log.info("\n".join(logs))
  2451. except subprocess.CalledProcessError as err:
  2452. _log.debug(
  2453. "Rebase FAILED: {cmd} returned code {code} with the "
  2454. "following output: {output}".format(
  2455. cmd=err.cmd, code=err.returncode, output=err.output
  2456. )
  2457. )
  2458. # This should never really happen, since we control the repos, but
  2459. # this way, we can be sure to get the output logged
  2460. remotes = []
  2461. for line in err.output.decode("utf-8").split("\n"):
  2462. _log.info("Remote line: %s", line)
  2463. if line.startswith("remote: "):
  2464. _log.debug("Remote: %s" % line)
  2465. remotes.append(line[len("remote: ") :].strip())
  2466. if remotes:
  2467. _log.info("Remote rejected with: %s" % remotes)
  2468. raise pagure.exceptions.PagurePushDenied(
  2469. "Remote hook declined the push: %s" % "\n".join(remotes)
  2470. )
  2471. else:
  2472. # Something else happened, pass the original
  2473. _log.exception("Error pushing. Output: %s", err.output)
  2474. raise
  2475. finally:
  2476. shutil.rmtree(repopath)