query.py 195 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2019 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. Farhaan Bukhsh <farhaan.bukhsh@gmail.com>
  7. """
  8. from __future__ import absolute_import, unicode_literals
  9. # pylint: disable=too-many-branches
  10. # pylint: disable=too-many-arguments
  11. # pylint: disable=too-many-locals
  12. # pylint: disable=too-many-statements
  13. # pylint: disable=too-many-lines
  14. try:
  15. import simplejson as json
  16. except ImportError: # pragma: no cover
  17. import json
  18. import copy
  19. import datetime
  20. import fnmatch
  21. import functools
  22. import hashlib
  23. import logging
  24. import os
  25. import shutil
  26. import subprocess
  27. import tempfile
  28. import uuid
  29. from collections import Counter
  30. from math import ceil
  31. import bleach
  32. import markdown
  33. import redis
  34. import six
  35. import sqlalchemy
  36. import sqlalchemy.schema
  37. import werkzeug.utils
  38. from flask import url_for
  39. from six.moves.urllib_parse import parse_qsl, urlencode, urlparse
  40. from sqlalchemy import Text, asc, cast, desc, func
  41. from sqlalchemy.orm import aliased
  42. import pagure.exceptions
  43. import pagure.lib.git
  44. import pagure.lib.git_auth
  45. import pagure.lib.link
  46. import pagure.lib.login
  47. import pagure.lib.notify
  48. import pagure.lib.plugins
  49. import pagure.lib.tasks
  50. import pagure.lib.tasks_services
  51. import pagure.pfmarkdown
  52. import pagure.utils
  53. from pagure.config import config as pagure_config
  54. from pagure.lib import model
  55. # For backward compatibility since this function used to be in this file
  56. from pagure.lib.model_base import create_session # noqa
  57. REDIS = None
  58. PAGURE_CI = None
  59. _log = logging.getLogger(__name__)
  60. # List of all the possible hooks pagure could generate before it was moved
  61. # to the runner architecture we now use.
  62. # This list is kept so we can ignore all of these hooks.
  63. ORIGINAL_PAGURE_HOOK = [
  64. "post-receive.default",
  65. "post-receive.fedmsg",
  66. "post-receive.irc",
  67. "post-receive.mail",
  68. "post-receive.mirror",
  69. "post-receive.pagure",
  70. "post-receive.pagure-requests",
  71. "post-receive.pagure-ticket",
  72. "post-receive.rtd",
  73. "pre-receive.pagure_no_new_branches",
  74. "pre-receive.pagureforcecommit",
  75. "pre-receive.pagureunsignedcommit",
  76. ]
  77. def get_repotypes():
  78. rt = ["main", "requests"]
  79. if pagure_config["ENABLE_TICKETS"]:
  80. rt.append("tickets")
  81. if pagure_config["ENABLE_DOCS"]:
  82. rt.append("docs")
  83. return tuple(rt)
  84. class Unspecified(object):
  85. """Custom None object used to indicate that the caller has not made
  86. a choice for a particular argument.
  87. """
  88. pass
  89. def set_redis(host, port, dbname):
  90. """Set the redis connection with the specified information."""
  91. global REDIS
  92. pool = redis.ConnectionPool(host=host, port=port, db=dbname)
  93. REDIS = redis.StrictRedis(connection_pool=pool)
  94. def set_pagure_ci(services):
  95. """Set the list of CI services supported by this pagure instance."""
  96. global PAGURE_CI
  97. PAGURE_CI = services
  98. def get_user(session, key):
  99. """Searches for a user in the database for a given username or email."""
  100. user_obj = search_user(session, username=key)
  101. if not user_obj:
  102. user_obj = search_user(session, email=key)
  103. if not user_obj:
  104. raise pagure.exceptions.PagureException('No user "%s" found' % key)
  105. return user_obj
  106. def get_user_by_id(session, userid):
  107. """Searches for a user in the database for a given username or email."""
  108. query = session.query(model.User).filter(model.User.id == userid)
  109. return query.first()
  110. def get_blocked_users(session, username=None, date=None):
  111. """Returns all the users that are blocked in this pagure instance."""
  112. now = datetime.datetime.utcnow()
  113. query = session.query(model.User).filter(
  114. model.User.refuse_sessions_before >= (date or now)
  115. )
  116. if username:
  117. if "*" in username:
  118. username = username.replace("*", "%")
  119. query = query.filter(model.User.user.ilike(username))
  120. else:
  121. query = query.filter(model.User.user == username)
  122. return query.all()
  123. def get_next_id(session, projectid):
  124. """Returns the next identifier of a project ticket or pull-request
  125. based on the identifier already in the database.
  126. """
  127. query1 = session.query(func.max(model.Issue.id)).filter(
  128. model.Issue.project_id == projectid
  129. )
  130. query2 = session.query(func.max(model.PullRequest.id)).filter(
  131. model.PullRequest.project_id == projectid
  132. )
  133. ids = [el[0] for el in query1.union(query2).all() if el[0] is not None]
  134. nid = 0
  135. if ids:
  136. nid = max(ids)
  137. return nid + 1
  138. def search_user(session, username=None, email=None, token=None, pattern=None):
  139. """Searches the database for the user or users matching the given
  140. criterias.
  141. :arg session: the session to use to connect to the database.
  142. :kwarg username: the username of the user to look for.
  143. :type username: string or None
  144. :kwarg email: the email or one of the email of the user to look for
  145. :type email: string or None
  146. :kwarg token: the token of the user to look for
  147. :type token: string or None
  148. :kwarg pattern: a pattern to search the users with.
  149. :type pattern: string or None
  150. :return: A single User object if any of username, email or token is
  151. specified, a list of User objects otherwise.
  152. :rtype: User or [User]
  153. """
  154. query = session.query(model.User).order_by(model.User.user)
  155. if username is not None:
  156. query = query.filter(model.User.user == username)
  157. if email is not None:
  158. query = query.filter(model.UserEmail.user_id == model.User.id).filter(
  159. model.UserEmail.email == email
  160. )
  161. if token is not None:
  162. query = query.filter(model.User.token == token)
  163. if pattern:
  164. pattern = pattern.replace("*", "%")
  165. query = query.filter(model.User.user.like(pattern))
  166. if any([username, email, token]):
  167. output = query.first()
  168. else:
  169. output = query.all()
  170. return output
  171. def is_valid_ssh_key(key, fp_hash="SHA256"):
  172. """Validates the ssh key using ssh-keygen."""
  173. key = key.strip()
  174. if not key:
  175. return None
  176. tmpdirname = tempfile.mkdtemp()
  177. filename = os.path.join(tmpdirname, "key")
  178. with open(filename, "w") as stream:
  179. stream.write(key)
  180. cmd = ["/usr/bin/ssh-keygen", "-l", "-f", filename, "-E", fp_hash]
  181. proc = subprocess.Popen(
  182. cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
  183. )
  184. stdout, stderr = proc.communicate()
  185. shutil.rmtree(tmpdirname)
  186. if proc.returncode != 0:
  187. _log.warning("STDOUT: %s", stdout)
  188. _log.warning("STDERR: %s", stderr)
  189. return False
  190. stdout = stdout.decode("utf-8")
  191. return stdout
  192. def are_valid_ssh_keys(keys):
  193. """Checks if all the ssh keys are valid or not."""
  194. return all(
  195. [is_valid_ssh_key(key) is not False for key in keys.split("\n")]
  196. )
  197. def find_ssh_key(session, search_key, username):
  198. """Finds and returns SSHKey matching the requested search_key.
  199. Args:
  200. session: database session
  201. search_key (string): The SSH fingerprint we are requested to look up
  202. username (string or None): If this is provided, the key is looked up
  203. to belong to the requested user.
  204. """
  205. query = session.query(model.SSHKey).filter(
  206. model.SSHKey.ssh_search_key == search_key
  207. )
  208. if username:
  209. userowner = (
  210. session.query(model.User.id)
  211. .filter(model.User.user == username)
  212. .subquery()
  213. )
  214. query = query.filter(model.SSHKey.user_id == userowner)
  215. try:
  216. return query.one()
  217. except sqlalchemy.orm.exc.NoResultFound:
  218. return None
  219. def create_deploykeys_ssh_keys_on_disk(project, gitolite_keydir):
  220. """Create the ssh keys for the projects' deploy keys on the key dir.
  221. This method does NOT support multiple ssh keys per deploy key.
  222. """
  223. if not gitolite_keydir:
  224. # Nothing to do here, move right along
  225. return
  226. # First remove deploykeys that no longer exist
  227. keyfiles = [
  228. "deploykey_%s_%s.pub"
  229. % (werkzeug.utils.secure_filename(project.fullname), key.id)
  230. for key in project.deploykeys
  231. ]
  232. project_key_dir = os.path.join(
  233. gitolite_keydir, "deploykeys", project.fullname
  234. )
  235. if not os.path.exists(project_key_dir):
  236. os.makedirs(project_key_dir)
  237. for keyfile in os.listdir(project_key_dir):
  238. if keyfile not in keyfiles:
  239. # This key is no longer in the project. Remove it.
  240. os.remove(os.path.join(project_key_dir, keyfile))
  241. for deploykey in project.deploykeys:
  242. # See the comment in lib/git.py:write_gitolite_acls about why this
  243. # name for a file is sane and does not inject a new security risk.
  244. keyfile = "deploykey_%s_%s.pub" % (
  245. werkzeug.utils.secure_filename(project.fullname),
  246. deploykey.id,
  247. )
  248. if not os.path.exists(os.path.join(project_key_dir, keyfile)):
  249. # We only take the very first key - deploykeys must be single keys
  250. key = deploykey.public_ssh_key.split("\n")[0]
  251. if not key:
  252. continue
  253. if not is_valid_ssh_key(key):
  254. continue
  255. with open(os.path.join(project_key_dir, keyfile), "w") as f:
  256. f.write(deploykey.public_ssh_key)
  257. def create_user_ssh_keys_on_disk(user, gitolite_keydir):
  258. """Create the ssh keys for the user on the specific folder.
  259. This is the method allowing to have multiple ssh keys per user.
  260. """
  261. if gitolite_keydir:
  262. # First remove any old keyfiles for the user
  263. # Assumption: we populated the keydir. This means that files
  264. # will be in 0/<username>.pub, ..., and not in any deeper
  265. # directory structures. Also, this means that if a user
  266. # had 5 lines, they will be up to at most keys_4/<username>.pub,
  267. # meaning that if a user is not in keys_<i>/<username>.pub, with
  268. # i being any integer, the user is most certainly not in
  269. # keys_<i+1>/<username>.pub.
  270. i = 0
  271. keyline_file = os.path.join(
  272. gitolite_keydir, "keys_%i" % i, "%s.pub" % user.user
  273. )
  274. while os.path.exists(keyline_file):
  275. os.unlink(keyline_file)
  276. i += 1
  277. keyline_file = os.path.join(
  278. gitolite_keydir, "keys_%i" % i, "%s.pub" % user.user
  279. )
  280. if not user.sshkeys:
  281. return
  282. # Now let's create new keyfiles for the user
  283. i = 0
  284. for key in user.sshkeys:
  285. if not is_valid_ssh_key(key.public_ssh_key):
  286. continue
  287. keyline_dir = os.path.join(gitolite_keydir, "keys_%i" % i)
  288. if not os.path.exists(keyline_dir):
  289. os.mkdir(keyline_dir)
  290. keyfile = os.path.join(keyline_dir, "%s.pub" % user.user)
  291. with open(keyfile, "w") as stream:
  292. stream.write(key.public_ssh_key.strip())
  293. i += 1
  294. def add_issue_comment(
  295. session,
  296. issue,
  297. comment,
  298. user,
  299. notify=True,
  300. date_created=None,
  301. notification=False,
  302. ):
  303. """Add a comment to an issue."""
  304. user_obj = get_user(session, user)
  305. issue_comment = model.IssueComment(
  306. issue_uid=issue.uid,
  307. comment=comment,
  308. user_id=user_obj.id,
  309. date_created=date_created,
  310. notification=notification,
  311. )
  312. issue.last_updated = datetime.datetime.utcnow()
  313. session.add(issue)
  314. session.add(issue_comment)
  315. # Make sure we won't have SQLAlchemy error before we continue
  316. session.commit()
  317. if not notification:
  318. log_action(session, "commented", issue, user_obj)
  319. if notify:
  320. pagure.lib.notify.notify_new_comment(issue_comment, user=user_obj)
  321. pagure.lib.git.update_git(issue, repo=issue.project)
  322. if not issue.private:
  323. pagure.lib.notify.log(
  324. issue.project,
  325. topic="issue.comment.added",
  326. msg=dict(
  327. issue=issue.to_json(public=True),
  328. project=issue.project.to_json(public=True),
  329. agent=user_obj.username,
  330. ),
  331. )
  332. # TODO: we should notify the SSE server even on update of the ticket
  333. # via git push to the ticket repo (the only case where notify=False
  334. # basically), but this causes problem with some of our markdown extension
  335. # so until we figure this out, we won't do live-refresh
  336. if REDIS and notify:
  337. if issue.private:
  338. REDIS.publish(
  339. "pagure.%s" % issue.uid,
  340. json.dumps(
  341. {"issue": "private", "comment_id": issue_comment.id}
  342. ),
  343. )
  344. else:
  345. REDIS.publish(
  346. "pagure.%s" % issue.uid,
  347. json.dumps(
  348. {
  349. "comment_id": issue_comment.id,
  350. "issue_id": issue.id,
  351. "project": issue.project.fullname,
  352. "comment_added": text2markdown(issue_comment.comment),
  353. "comment_user": issue_comment.user.user,
  354. "avatar_url": avatar_url_from_email(
  355. issue_comment.user.default_email, size=16
  356. ),
  357. "comment_date": issue_comment.date_created.strftime(
  358. "%Y-%m-%d %H:%M:%S"
  359. ),
  360. "notification": notification,
  361. }
  362. ),
  363. )
  364. return "Comment added"
  365. def add_tag_obj(session, obj, tags, user):
  366. """Add a tag to an object (either an issue or a project)."""
  367. user_obj = get_user(session, user)
  368. if isinstance(tags, six.string_types):
  369. tags = [tags]
  370. added_tags = []
  371. added_tags_color = []
  372. for objtag in tags:
  373. objtag = objtag.strip()
  374. known = False
  375. for tagobj in obj.tags:
  376. if tagobj.tag == objtag:
  377. known = True
  378. if known:
  379. continue
  380. if obj.isa == "project":
  381. tagobj = get_tag(session, objtag)
  382. if not tagobj:
  383. tagobj = model.Tag(tag=objtag)
  384. session.add(tagobj)
  385. session.flush()
  386. dbobjtag = model.TagProject(project_id=obj.id, tag=tagobj.tag)
  387. else:
  388. tagobj = get_colored_tag(session, objtag, obj.project.id)
  389. if not tagobj:
  390. tagobj = model.TagColored(
  391. tag=objtag, project_id=obj.project.id
  392. )
  393. session.add(tagobj)
  394. session.flush()
  395. if obj.isa == "issue":
  396. dbobjtag = model.TagIssueColored(
  397. issue_uid=obj.uid, tag_id=tagobj.id
  398. )
  399. else:
  400. dbobjtag = model.TagPullRequest(
  401. request_uid=obj.uid, tag_id=tagobj.id
  402. )
  403. added_tags_color.append(tagobj.tag_color)
  404. session.add(dbobjtag)
  405. # Make sure we won't have SQLAlchemy error before we continue
  406. # Commit so the tags show up in the notification sent
  407. session.commit()
  408. added_tags.append(tagobj.tag)
  409. if isinstance(obj, model.Issue):
  410. pagure.lib.git.update_git(obj, repo=obj.project)
  411. if not obj.private:
  412. pagure.lib.notify.log(
  413. obj.project,
  414. topic="issue.tag.added",
  415. msg=dict(
  416. issue=obj.to_json(public=True),
  417. project=obj.project.to_json(public=True),
  418. tags=sorted(added_tags),
  419. agent=user_obj.username,
  420. ),
  421. )
  422. # Send notification for the event-source server
  423. if REDIS and not obj.project.private:
  424. REDIS.publish(
  425. "pagure.%s" % obj.uid,
  426. json.dumps(
  427. {
  428. "added_tags": added_tags,
  429. "added_tags_color": added_tags_color,
  430. }
  431. ),
  432. )
  433. elif isinstance(obj, model.PullRequest):
  434. pagure.lib.git.update_git(obj, repo=obj.project)
  435. if not obj.private:
  436. pagure.lib.notify.log(
  437. obj.project,
  438. topic="pull-request.tag.added",
  439. msg=dict(
  440. pull_request=obj.to_json(public=True),
  441. pullrequest=obj.to_json(public=True),
  442. project=obj.project.to_json(public=True),
  443. tags=sorted(added_tags),
  444. agent=user_obj.username,
  445. ),
  446. )
  447. # Send notification for the event-source server
  448. if REDIS and not obj.project.private:
  449. REDIS.publish(
  450. "pagure.%s" % obj.uid,
  451. json.dumps(
  452. {
  453. "added_tags": added_tags,
  454. "added_tags_color": added_tags_color,
  455. }
  456. ),
  457. )
  458. if added_tags:
  459. return "%s tagged with: %s" % (
  460. obj.isa.capitalize(),
  461. ", ".join(added_tags),
  462. )
  463. else:
  464. return "Nothing to add"
  465. def add_issue_assignee(session, issue, assignee, user, notify=True):
  466. """Add an assignee to an issue, in other words, assigned an issue."""
  467. user_obj = get_user(session, user)
  468. old_assignee = issue.assignee
  469. if not assignee and issue.assignee is not None:
  470. issue.assignee_id = None
  471. issue.last_updated = datetime.datetime.utcnow()
  472. session.add(issue)
  473. session.commit()
  474. pagure.lib.git.update_git(issue, repo=issue.project)
  475. if notify:
  476. pagure.lib.notify.notify_assigned_issue(issue, None, user_obj)
  477. if not issue.private:
  478. pagure.lib.notify.log(
  479. issue.project,
  480. topic="issue.assigned.reset",
  481. msg=dict(
  482. issue=issue.to_json(public=True),
  483. project=issue.project.to_json(public=True),
  484. agent=user_obj.username,
  485. ),
  486. )
  487. # Send notification for the event-source server
  488. if REDIS and not issue.project.private:
  489. REDIS.publish(
  490. "pagure.%s" % issue.uid, json.dumps({"unassigned": "-"})
  491. )
  492. return "Assignee reset"
  493. elif not assignee and issue.assignee is None:
  494. return
  495. old_assignee = issue.assignee
  496. # Validate the assignee
  497. assignee_obj = get_user(session, assignee)
  498. if issue.assignee_id != assignee_obj.id:
  499. issue.assignee_id = assignee_obj.id
  500. session.add(issue)
  501. session.commit()
  502. pagure.lib.git.update_git(issue, repo=issue.project)
  503. if notify:
  504. pagure.lib.notify.notify_assigned_issue(
  505. issue, assignee_obj, user_obj
  506. )
  507. if not issue.private:
  508. pagure.lib.notify.log(
  509. issue.project,
  510. topic="issue.assigned.added",
  511. msg=dict(
  512. issue=issue.to_json(public=True),
  513. project=issue.project.to_json(public=True),
  514. agent=user_obj.username,
  515. ),
  516. )
  517. issue.last_updated = datetime.datetime.utcnow()
  518. # Send notification for the event-source server
  519. if REDIS and not issue.project.private:
  520. REDIS.publish(
  521. "pagure.%s" % issue.uid,
  522. json.dumps({"assigned": assignee_obj.to_json(public=True)}),
  523. )
  524. output = "Issue assigned to %s" % assignee
  525. if old_assignee:
  526. output += " (was: %s)" % old_assignee.username
  527. return output
  528. def add_pull_request_assignee(session, request, assignee, user):
  529. """Add an assignee to a request, in other words, assigned an issue."""
  530. get_user(session, assignee)
  531. user_obj = get_user(session, user)
  532. if assignee is None and request.assignee is not None:
  533. request.assignee_id = None
  534. request.updated_on = datetime.datetime.utcnow()
  535. session.add(request)
  536. session.commit()
  537. pagure.lib.git.update_git(request, repo=request.project)
  538. pagure.lib.notify.notify_assigned_request(request, None, user_obj)
  539. # Deprecated -- this is not consistent in both the topic and the body:
  540. # request vs pull-request (topic) and request vs pullrequest (body)
  541. pagure.lib.notify.log(
  542. request.project,
  543. topic="request.assigned.reset",
  544. msg=dict(
  545. request=request.to_json(public=True),
  546. pullrequest=request.to_json(public=True),
  547. project=request.project.to_json(public=True),
  548. agent=user_obj.username,
  549. ),
  550. )
  551. pagure.lib.notify.log(
  552. request.project,
  553. topic="pull-request.assigned.reset",
  554. msg=dict(
  555. pullrequest=request.to_json(public=True),
  556. project=request.project.to_json(public=True),
  557. agent=user_obj.username,
  558. ),
  559. )
  560. return "Request assignee reset"
  561. elif assignee is None and request.assignee is None:
  562. return
  563. # Validate the assignee
  564. assignee_obj = get_user(session, assignee)
  565. if request.assignee_id != assignee_obj.id:
  566. request.assignee_id = assignee_obj.id
  567. request.updated_on = datetime.datetime.utcnow()
  568. session.add(request)
  569. session.flush()
  570. pagure.lib.git.update_git(request, repo=request.project)
  571. pagure.lib.notify.notify_assigned_request(
  572. request, assignee_obj, user_obj
  573. )
  574. # Deprecated -- this is not consistent in both the topic and the body:
  575. # request vs pull-request (topic) and request vs pullrequest (body)
  576. pagure.lib.notify.log(
  577. request.project,
  578. topic="request.assigned.added",
  579. msg=dict(
  580. request=request.to_json(public=True),
  581. pullrequest=request.to_json(public=True),
  582. project=request.project.to_json(public=True),
  583. agent=user_obj.username,
  584. ),
  585. )
  586. pagure.lib.notify.log(
  587. request.project,
  588. topic="pull-request.assigned.added",
  589. msg=dict(
  590. pullrequest=request.to_json(public=True),
  591. project=request.project.to_json(public=True),
  592. agent=user_obj.username,
  593. ),
  594. )
  595. return "Request assigned"
  596. def add_issue_dependency(session, issue, issue_blocked, user):
  597. """Add a dependency between two issues."""
  598. user_obj = get_user(session, user)
  599. if issue.uid == issue_blocked.uid:
  600. raise pagure.exceptions.PagureException(
  601. "An issue cannot depend on itself"
  602. )
  603. if issue_blocked not in issue.children:
  604. i2i = model.IssueToIssue(
  605. parent_issue_id=issue.uid, child_issue_id=issue_blocked.uid
  606. )
  607. session.add(i2i)
  608. # Make sure we won't have SQLAlchemy error before we continue
  609. # and commit so the blocking issue appears in the JSON representation
  610. session.commit()
  611. pagure.lib.git.update_git(issue, repo=issue.project)
  612. pagure.lib.git.update_git(issue_blocked, repo=issue_blocked.project)
  613. if not issue.private:
  614. pagure.lib.notify.log(
  615. issue.project,
  616. topic="issue.dependency.added",
  617. msg=dict(
  618. issue=issue.to_json(public=True),
  619. project=issue.project.to_json(public=True),
  620. added_dependency=issue_blocked.id,
  621. agent=user_obj.username,
  622. ),
  623. )
  624. # Send notification for the event-source server
  625. if REDIS and not issue.project.private:
  626. REDIS.publish(
  627. "pagure.%s" % issue.uid,
  628. json.dumps(
  629. {
  630. "added_dependency": issue_blocked.id,
  631. "issue_uid": issue.uid,
  632. "type": "children",
  633. }
  634. ),
  635. )
  636. REDIS.publish(
  637. "pagure.%s" % issue_blocked.uid,
  638. json.dumps(
  639. {
  640. "added_dependency": issue.id,
  641. "issue_uid": issue_blocked.uid,
  642. "type": "parent",
  643. }
  644. ),
  645. )
  646. return "Issue marked as depending on: #%s" % issue_blocked.id
  647. def remove_issue_dependency(session, issue, issue_blocked, user):
  648. """Remove a dependency between two issues."""
  649. user_obj = get_user(session, user)
  650. if issue.uid == issue_blocked.uid:
  651. raise pagure.exceptions.PagureException(
  652. "An issue cannot depend on itself"
  653. )
  654. if issue_blocked in issue.parents:
  655. parent_del = []
  656. for parent in issue.parents:
  657. if parent.uid == issue_blocked.uid:
  658. parent_del.append(parent.id)
  659. issue.parents.remove(parent)
  660. # Make sure we won't have SQLAlchemy error before we continue
  661. # and commit so the blocking issue appears in the JSON representation
  662. session.commit()
  663. pagure.lib.git.update_git(issue, repo=issue.project)
  664. pagure.lib.git.update_git(issue_blocked, repo=issue_blocked.project)
  665. if not issue.private:
  666. pagure.lib.notify.log(
  667. issue.project,
  668. topic="issue.dependency.removed",
  669. msg=dict(
  670. issue=issue.to_json(public=True),
  671. project=issue.project.to_json(public=True),
  672. removed_dependency=parent_del,
  673. agent=user_obj.username,
  674. ),
  675. )
  676. # Send notification for the event-source server
  677. if REDIS and not issue.project.private:
  678. REDIS.publish(
  679. "pagure.%s" % issue.uid,
  680. json.dumps(
  681. {
  682. "removed_dependency": parent_del,
  683. "issue_uid": issue.uid,
  684. "type": "children",
  685. }
  686. ),
  687. )
  688. REDIS.publish(
  689. "pagure.%s" % issue_blocked.uid,
  690. json.dumps(
  691. {
  692. "removed_dependency": issue.id,
  693. "issue_uid": issue_blocked.uid,
  694. "type": "parent",
  695. }
  696. ),
  697. )
  698. return "Issue **un**marked as depending on: #%s" % " #".join(
  699. [("%s" % id) for id in parent_del]
  700. )
  701. def remove_tags(session, project, tags, user):
  702. """Removes the specified tag of a project."""
  703. user_obj = get_user(session, user)
  704. if not isinstance(tags, list):
  705. tags = [tags]
  706. issues = search_issues(session, project, closed=False, tags=tags)
  707. issues.extend(search_issues(session, project, closed=True, tags=tags))
  708. msgs = []
  709. removed_tags = []
  710. tag_found = False
  711. for tag in tags:
  712. tagobj = get_colored_tag(session, tag, project.id)
  713. if tagobj:
  714. tag_found = True
  715. removed_tags.append(tag)
  716. msgs.append("Tag: %s has been deleted" % tag)
  717. session.delete(tagobj)
  718. if not tag_found:
  719. raise pagure.exceptions.PagureException(
  720. "Tags not found: %s" % ", ".join(tags)
  721. )
  722. for issue in issues:
  723. for issue_tag in issue.tags:
  724. if issue_tag.tag in tags:
  725. tag = issue_tag.tag
  726. session.delete(issue_tag)
  727. pagure.lib.git.update_git(issue, repo=issue.project)
  728. pagure.lib.notify.log(
  729. project,
  730. topic="project.tag.removed",
  731. msg=dict(
  732. project=project.to_json(public=True),
  733. tags=sorted(removed_tags),
  734. agent=user_obj.username,
  735. ),
  736. )
  737. return msgs
  738. def remove_tags_obj(session, obj, tags, user):
  739. """Removes the specified tag(s) of a given object."""
  740. user_obj = get_user(session, user)
  741. if isinstance(tags, six.string_types):
  742. tags = [tags]
  743. removed_tags = []
  744. if obj.isa == "project":
  745. for objtag in obj.tags:
  746. if objtag.tag in tags:
  747. tag = objtag.tag
  748. removed_tags.append(tag)
  749. session.delete(objtag)
  750. elif obj.isa == "issue":
  751. for objtag in obj.tags_issues_colored:
  752. if objtag.tag.tag in tags:
  753. tag = objtag.tag.tag
  754. removed_tags.append(tag)
  755. session.delete(objtag)
  756. elif obj.isa == "pull-request":
  757. for objtag in obj.tags_pr_colored:
  758. if objtag.tag.tag in tags:
  759. tag = objtag.tag.tag
  760. removed_tags.append(tag)
  761. session.delete(objtag)
  762. # Commit so the tags are updated in the notification sent
  763. session.commit()
  764. if isinstance(obj, model.Issue):
  765. pagure.lib.git.update_git(obj, repo=obj.project)
  766. pagure.lib.notify.log(
  767. obj.project,
  768. topic="issue.tag.removed",
  769. msg=dict(
  770. issue=obj.to_json(public=True),
  771. project=obj.project.to_json(public=True),
  772. tags=sorted(removed_tags),
  773. agent=user_obj.username,
  774. ),
  775. )
  776. # Send notification for the event-source server
  777. if REDIS and not obj.project.private:
  778. REDIS.publish(
  779. "pagure.%s" % obj.uid,
  780. json.dumps({"removed_tags": removed_tags}),
  781. )
  782. elif isinstance(obj, model.PullRequest):
  783. pagure.lib.git.update_git(obj, repo=obj.project)
  784. pagure.lib.notify.log(
  785. obj.project,
  786. topic="pull-request.tag.removed",
  787. msg=dict(
  788. pull_request=obj.to_json(public=True),
  789. pullrequest=obj.to_json(public=True),
  790. project=obj.project.to_json(public=True),
  791. tags=sorted(removed_tags),
  792. agent=user_obj.username,
  793. ),
  794. )
  795. # Send notification for the event-source server
  796. if REDIS and not obj.project.private:
  797. REDIS.publish(
  798. "pagure.%s" % obj.uid,
  799. json.dumps({"removed_tags": removed_tags}),
  800. )
  801. return "%s **un**tagged with: %s" % (
  802. obj.isa.capitalize(),
  803. ", ".join(removed_tags),
  804. )
  805. def edit_issue_tags(
  806. session,
  807. project,
  808. old_tag,
  809. new_tag,
  810. new_tag_description,
  811. new_tag_color,
  812. user,
  813. ):
  814. """Edits the specified tag of a project."""
  815. user_obj = get_user(session, user)
  816. old_tag_name = old_tag
  817. if not isinstance(old_tag, model.TagColored):
  818. old_tag = get_colored_tag(session, old_tag_name, project.id)
  819. if not old_tag:
  820. raise pagure.exceptions.PagureException(
  821. 'No tag "%s" found related to this project' % (old_tag_name)
  822. )
  823. old_tag_name = old_tag.tag
  824. old_tag_description = old_tag.tag_description
  825. old_tag_color = old_tag.tag_color
  826. # check for change
  827. no_change_in_tag = (
  828. old_tag.tag == new_tag
  829. and old_tag_description == new_tag_description
  830. and old_tag_color == new_tag_color
  831. )
  832. if no_change_in_tag:
  833. raise pagure.exceptions.PagureException(
  834. 'No change. Old tag "%s(%s)[%s]" is the same as '
  835. 'new tag "%s(%s)[%s]"'
  836. % (
  837. old_tag,
  838. old_tag_description,
  839. old_tag_color,
  840. new_tag,
  841. new_tag_description,
  842. new_tag_color,
  843. )
  844. )
  845. elif old_tag.tag != new_tag:
  846. # Check if new tag already exists
  847. existing_tag = get_colored_tag(session, new_tag, project.id)
  848. if existing_tag:
  849. raise pagure.exceptions.PagureException(
  850. "Can not rename a tag to an existing tag name: %s" % new_tag
  851. )
  852. session.query(model.TagColored).filter(
  853. model.TagColored.tag == old_tag.tag
  854. ).filter(model.TagColored.project_id == project.id).update(
  855. {
  856. model.TagColored.tag: new_tag,
  857. model.TagColored.tag_description: new_tag_description,
  858. model.TagColored.tag_color: new_tag_color,
  859. }
  860. )
  861. issues = (
  862. session.query(model.Issue)
  863. .filter(model.TagIssueColored.tag_id == old_tag.id)
  864. .filter(model.TagIssueColored.issue_uid == model.Issue.uid)
  865. .all()
  866. )
  867. for issue in issues:
  868. # Update the git version
  869. pagure.lib.git.update_git(issue, repo=issue.project)
  870. msgs = []
  871. msgs.append(
  872. "Edited tag: %s(%s)[%s] to %s(%s)[%s]"
  873. % (
  874. old_tag_name,
  875. old_tag_description,
  876. old_tag_color,
  877. new_tag,
  878. new_tag_description,
  879. new_tag_color,
  880. )
  881. )
  882. pagure.lib.notify.log(
  883. project,
  884. topic="project.tag.edited",
  885. msg=dict(
  886. project=project.to_json(public=True),
  887. old_tag=old_tag.tag,
  888. old_tag_description=old_tag_description,
  889. old_tag_color=old_tag_color,
  890. new_tag=new_tag,
  891. new_tag_description=new_tag_description,
  892. new_tag_color=new_tag_color,
  893. agent=user_obj.username,
  894. ),
  895. )
  896. return msgs
  897. def add_sshkey_to_project_or_user(
  898. session, ssh_key, pushaccess, creator, project=None, user=None
  899. ):
  900. """Add a deploy key to a specified project."""
  901. if project is None and user is None:
  902. raise ValueError(
  903. "SSH Keys need to be added to either a project or a user"
  904. )
  905. if project is not None and user is not None:
  906. raise ValueError("SSH Keys need to be assigned to at least one object")
  907. ssh_key = ssh_key.strip()
  908. if "\n" in ssh_key:
  909. raise pagure.exceptions.PagureException("Please add single SSH keys.")
  910. ssh_short_key = is_valid_ssh_key(ssh_key)
  911. if ssh_short_key in [None, False]:
  912. raise pagure.exceptions.PagureException("SSH key invalid.")
  913. # We are sure that this only contains a single key, but ssh-keygen still
  914. # returns a \n at the end
  915. ssh_short_key = ssh_short_key.strip()
  916. if "\n" in ssh_key:
  917. raise pagure.exceptions.PagureException(
  918. "SSH has misbehaved when analyzing the SSH key"
  919. )
  920. # Make sure that this key is not an SSH key for another project or user.
  921. # If we dupe keys, we can't really know who this is for.
  922. ssh_search_key = ssh_short_key.split(" ")[1]
  923. if (
  924. session.query(model.SSHKey)
  925. .filter(model.SSHKey.ssh_search_key == ssh_search_key)
  926. .count()
  927. != 0
  928. ):
  929. raise pagure.exceptions.PagureException("SSH key already exists.")
  930. new_key_obj = model.SSHKey(
  931. pushaccess=pushaccess,
  932. public_ssh_key=ssh_key,
  933. ssh_short_key=ssh_short_key,
  934. ssh_search_key=ssh_search_key,
  935. creator_user_id=creator.id,
  936. )
  937. if project:
  938. new_key_obj.project = project
  939. if user:
  940. new_key_obj.user_id = user.id
  941. session.add(new_key_obj)
  942. # Make sure we won't have SQLAlchemy error before we continue
  943. session.flush()
  944. # We do not send any notifications on purpose
  945. return "SSH key added"
  946. def add_user_to_project(
  947. session,
  948. project,
  949. new_user,
  950. user,
  951. access="admin",
  952. branches=None,
  953. required_groups=None,
  954. ):
  955. """Add a specified user to a specified project with a specified access"""
  956. new_user_obj = get_user(session, new_user)
  957. if required_groups and access != "ticket":
  958. for key in required_groups:
  959. if fnmatch.fnmatch(project.fullname, key):
  960. user_grps = set(new_user_obj.groups)
  961. req_grps = set(required_groups[key])
  962. if not user_grps.intersection(req_grps):
  963. raise pagure.exceptions.PagureException(
  964. "This user must be in one of the following groups "
  965. "to be allowed to be added to this project: %s"
  966. % ", ".join(req_grps)
  967. )
  968. user_obj = get_user(session, user)
  969. users = set(
  970. [
  971. user_.user
  972. for user_ in project.get_project_users(access, combine=False)
  973. ]
  974. )
  975. users.add(project.user.user)
  976. if new_user in users and access != "collaborator":
  977. raise pagure.exceptions.PagureException(
  978. "This user is already listed on this project with the same access"
  979. )
  980. # Reset the branches to None if the user isn't a collaborator
  981. if access != "collaborator":
  982. branches = None
  983. # user has some access on project, so update to new access
  984. if new_user_obj in project.users:
  985. access_obj = get_obj_access(session, project, new_user_obj)
  986. access_obj.access = access
  987. access_obj.branches = branches
  988. project.date_modified = datetime.datetime.utcnow()
  989. update_read_only_mode(session, project, read_only=True)
  990. session.add(access_obj)
  991. session.add(project)
  992. session.commit()
  993. pagure.lib.notify.log(
  994. project,
  995. topic="project.user.access.updated",
  996. msg=dict(
  997. project=project.to_json(public=True),
  998. new_user=new_user_obj.username,
  999. new_access=access,
  1000. new_branches=branches,
  1001. agent=user_obj.username,
  1002. ),
  1003. )
  1004. return "User access updated"
  1005. project_user = model.ProjectUser(
  1006. project_id=project.id,
  1007. user_id=new_user_obj.id,
  1008. access=access,
  1009. branches=branches,
  1010. )
  1011. project.date_modified = datetime.datetime.utcnow()
  1012. session.add(project_user)
  1013. # Mark the project as read only, celery will then unmark it
  1014. update_read_only_mode(session, project, read_only=True)
  1015. session.add(project)
  1016. # Commit so the JSON sent in the notification is up to date
  1017. session.commit()
  1018. pagure.lib.notify.log(
  1019. project,
  1020. topic="project.user.added",
  1021. msg=dict(
  1022. project=project.to_json(public=True),
  1023. new_user=new_user_obj.username,
  1024. access=access,
  1025. branches=branches,
  1026. agent=user_obj.username,
  1027. ),
  1028. )
  1029. return "User added"
  1030. def add_group_to_project(
  1031. session,
  1032. project,
  1033. new_group,
  1034. user,
  1035. access="admin",
  1036. branches=None,
  1037. create=False,
  1038. is_admin=False,
  1039. ):
  1040. """Add a specified group to a specified project with some access"""
  1041. user_obj = search_user(session, username=user)
  1042. if not user_obj:
  1043. raise pagure.exceptions.PagureException("No user %s found." % user)
  1044. group_obj = search_groups(session, group_name=new_group)
  1045. if not group_obj:
  1046. if create:
  1047. group_obj = pagure.lib.model.PagureGroup(
  1048. group_name=new_group,
  1049. display_name=new_group,
  1050. group_type="user",
  1051. user_id=user_obj.id,
  1052. )
  1053. session.add(group_obj)
  1054. session.flush()
  1055. else:
  1056. raise pagure.exceptions.PagureException(
  1057. "No group %s found." % new_group
  1058. )
  1059. if (
  1060. user_obj not in project.users
  1061. and user_obj != project.user
  1062. and not is_admin
  1063. ):
  1064. raise pagure.exceptions.PagureException(
  1065. "You are not allowed to add a group of users to this project"
  1066. )
  1067. groups = set(
  1068. [
  1069. group.group_name
  1070. for group in project.get_project_groups(access, combine=False)
  1071. ]
  1072. )
  1073. if new_group in groups and access != "collaborator":
  1074. raise pagure.exceptions.PagureException(
  1075. "This group already has this access on this project"
  1076. )
  1077. # Reset the branches to None if the group isn't a collaborator
  1078. if access != "collaborator":
  1079. branches = None
  1080. # the group already has some access, update to new access
  1081. if group_obj in project.groups:
  1082. access_obj = get_obj_access(session, project, group_obj)
  1083. access_obj.access = access
  1084. access_obj.branches = branches
  1085. session.add(access_obj)
  1086. project.date_modified = datetime.datetime.utcnow()
  1087. update_read_only_mode(session, project, read_only=True)
  1088. session.add(project)
  1089. # Commit so the JSON sent in the notification is up to date
  1090. session.commit()
  1091. pagure.lib.notify.log(
  1092. project,
  1093. topic="project.group.access.updated",
  1094. msg=dict(
  1095. project=project.to_json(public=True),
  1096. new_group=group_obj.group_name,
  1097. new_access=access,
  1098. new_branches=branches,
  1099. agent=user,
  1100. ),
  1101. )
  1102. return "Group access updated"
  1103. project_group = model.ProjectGroup(
  1104. project_id=project.id,
  1105. group_id=group_obj.id,
  1106. access=access,
  1107. branches=branches,
  1108. )
  1109. session.add(project_group)
  1110. # Make sure we won't have SQLAlchemy error before we continue
  1111. project.date_modified = datetime.datetime.utcnow()
  1112. # Mark the project read_only, celery will then unmark it
  1113. update_read_only_mode(session, project, read_only=True)
  1114. session.add(project)
  1115. # Commit so the JSON sent in the notification is up to date
  1116. session.commit()
  1117. pagure.lib.notify.log(
  1118. project,
  1119. topic="project.group.added",
  1120. msg=dict(
  1121. project=project.to_json(public=True),
  1122. new_group=group_obj.group_name,
  1123. access=access,
  1124. branches=branches,
  1125. agent=user,
  1126. ),
  1127. )
  1128. return "Group added"
  1129. def add_pull_request_comment(
  1130. session,
  1131. request,
  1132. commit,
  1133. tree_id,
  1134. filename,
  1135. row,
  1136. comment,
  1137. user,
  1138. notify=True,
  1139. notification=False,
  1140. trigger_ci=None,
  1141. ):
  1142. """Add a comment to a pull-request."""
  1143. user_obj = get_user(session, user)
  1144. pr_comment = model.PullRequestComment(
  1145. pull_request_uid=request.uid,
  1146. commit_id=commit,
  1147. tree_id=tree_id,
  1148. filename=filename,
  1149. line=row,
  1150. comment=comment,
  1151. user_id=user_obj.id,
  1152. notification=notification,
  1153. )
  1154. session.add(pr_comment)
  1155. # Make sure we won't have SQLAlchemy error before we continue
  1156. session.flush()
  1157. request.updated_on = datetime.datetime.utcnow()
  1158. pagure.lib.git.update_git(request, repo=request.project)
  1159. log_action(session, "commented", request, user_obj)
  1160. if notify:
  1161. pagure.lib.notify.notify_pull_request_comment(pr_comment, user_obj)
  1162. # Send notification for the event-source server
  1163. if REDIS and not request.project.private:
  1164. comment_text = text2markdown(pr_comment.comment)
  1165. REDIS.publish(
  1166. "pagure.%s" % request.uid,
  1167. json.dumps(
  1168. {
  1169. "request_id": request.id,
  1170. "comment_added": comment_text,
  1171. "comment_user": pr_comment.user.user,
  1172. "comment_id": pr_comment.id,
  1173. "project": request.project.fullname,
  1174. "avatar_url": avatar_url_from_email(
  1175. pr_comment.user.default_email, size=16
  1176. ),
  1177. "comment_date": pr_comment.date_created.strftime(
  1178. "%Y-%m-%d %H:%M:%S"
  1179. ),
  1180. "commit_id": commit,
  1181. "filename": filename,
  1182. "line": row,
  1183. "notification": notification,
  1184. }
  1185. ),
  1186. )
  1187. # Send notification to the CI server, if the comment added was a
  1188. # notification and the PR is still open and project is not private
  1189. ci_hook = pagure.lib.plugins.get_plugin("Pagure CI")
  1190. ci_hook.db_object()
  1191. ci_triggered = False
  1192. if (
  1193. notification
  1194. and request.status == "Open"
  1195. and pagure_config.get("PAGURE_CI_SERVICES")
  1196. and request.project.ci_hook
  1197. and request.project.ci_hook.active_pr
  1198. and not request.project.private
  1199. ):
  1200. pagure.lib.tasks_services.trigger_ci_build.delay(
  1201. pr_uid=request.uid,
  1202. cause=request.id,
  1203. branch=request.branch_from,
  1204. branch_to=request.branch,
  1205. ci_type=request.project.ci_hook.ci_type,
  1206. )
  1207. ci_triggered = True
  1208. pagure.lib.notify.log(
  1209. request.project,
  1210. topic="pull-request.comment.added",
  1211. msg=dict(
  1212. pullrequest=request.to_json(public=True), agent=user_obj.username
  1213. ),
  1214. )
  1215. if (
  1216. not ci_triggered
  1217. and trigger_ci
  1218. and comment.strip().lower() in trigger_ci
  1219. and pagure_config.get("PAGURE_CI_SERVICES")
  1220. and request.project.ci_hook
  1221. and request.project.ci_hook.active_pr
  1222. ):
  1223. pagure.lib.tasks_services.trigger_ci_build.delay(
  1224. pr_uid=request.uid,
  1225. cause=request.id,
  1226. branch=request.branch_from,
  1227. branch_to=request.branch,
  1228. ci_type=request.project.ci_hook.ci_type,
  1229. )
  1230. return "Comment added"
  1231. def edit_comment(session, parent, comment, user, updated_comment):
  1232. """Edit a comment."""
  1233. user_obj = get_user(session, user)
  1234. comment.comment = updated_comment
  1235. comment.edited_on = datetime.datetime.utcnow()
  1236. comment.editor = user_obj
  1237. parent.last_updated = comment.edited_on
  1238. session.add(parent)
  1239. session.add(comment)
  1240. # Make sure we won't have SQLAlchemy error before we continue
  1241. session.flush()
  1242. pagure.lib.git.update_git(parent, repo=parent.project)
  1243. topic = "unknown"
  1244. key = "unknown"
  1245. id_ = "unknown"
  1246. private = False
  1247. if parent.isa == "pull-request":
  1248. topic = "pull-request.comment.edited"
  1249. key = "pullrequest"
  1250. id_ = "request_id"
  1251. elif parent.isa == "issue":
  1252. topic = "issue.comment.edited"
  1253. key = "issue"
  1254. id_ = "issue_id"
  1255. private = parent.private
  1256. if not private:
  1257. pagure.lib.notify.log(
  1258. parent.project,
  1259. topic=topic,
  1260. msg={
  1261. key: parent.to_json(public=True, with_comments=False),
  1262. "project": parent.project.to_json(public=True),
  1263. "comment": comment.to_json(public=True),
  1264. "agent": user_obj.username,
  1265. },
  1266. )
  1267. if REDIS and not parent.project.private:
  1268. if private:
  1269. REDIS.publish(
  1270. "pagure.%s" % comment.parent.uid,
  1271. json.dumps(
  1272. {"comment_updated": "private", "comment_id": comment.id}
  1273. ),
  1274. )
  1275. else:
  1276. REDIS.publish(
  1277. "pagure.%s" % parent.uid,
  1278. json.dumps(
  1279. {
  1280. id_: len(parent.comments),
  1281. "comment_updated": text2markdown(comment.comment),
  1282. "comment_id": comment.id,
  1283. "parent_id": comment.parent.id,
  1284. "comment_editor": user_obj.user,
  1285. "avatar_url": avatar_url_from_email(
  1286. comment.user.default_email, size=16
  1287. ),
  1288. "comment_date": comment.edited_on.strftime(
  1289. "%Y-%m-%d %H:%M:%S"
  1290. ),
  1291. }
  1292. ),
  1293. )
  1294. return "Comment updated"
  1295. def add_pull_request_flag(
  1296. session, request, username, percent, comment, url, status, uid, user, token
  1297. ):
  1298. """Add a flag to a pull-request."""
  1299. user_obj = get_user(session, user)
  1300. action, flag_uid = add_commit_flag(
  1301. session=session,
  1302. repo=request.project,
  1303. commit_hash=request.commit_stop,
  1304. username=username,
  1305. status=status,
  1306. percent=percent,
  1307. comment=comment,
  1308. url=url,
  1309. uid=uid,
  1310. user=user,
  1311. token=token,
  1312. )
  1313. action = action.replace("Flag ", "")
  1314. pr_flag = pagure.lib.query.get_commit_flag_by_uid(
  1315. session, request.commit_stop, flag_uid
  1316. )
  1317. if request.project.settings.get("notify_on_pull-request_flag"):
  1318. pagure.lib.notify.notify_pull_request_flag(pr_flag, request, username)
  1319. pagure.lib.git.update_git(request, repo=request.project)
  1320. pagure.lib.notify.log(
  1321. request.project,
  1322. topic="pull-request.flag.%s" % action,
  1323. msg=dict(
  1324. pullrequest=request.to_json(public=True),
  1325. flag=pr_flag.to_json(public=True),
  1326. agent=user_obj.username,
  1327. ),
  1328. )
  1329. return ("Flag %s" % action, pr_flag.uid)
  1330. def add_commit_flag(
  1331. session,
  1332. repo,
  1333. commit_hash,
  1334. username,
  1335. status,
  1336. percent,
  1337. comment,
  1338. url,
  1339. uid,
  1340. user,
  1341. token,
  1342. ):
  1343. """Add a flag to a add_commit_flag."""
  1344. user_obj = get_user(session, user)
  1345. action = "added"
  1346. c_flag = get_commit_flag_by_uid(session, commit_hash, uid)
  1347. if c_flag:
  1348. action = "updated"
  1349. c_flag.comment = comment
  1350. c_flag.percent = percent
  1351. c_flag.status = status
  1352. c_flag.url = url
  1353. else:
  1354. c_flag = model.CommitFlag(
  1355. uid=uid or uuid.uuid4().hex,
  1356. project_id=repo.id,
  1357. commit_hash=commit_hash,
  1358. username=username,
  1359. status=status,
  1360. percent=percent,
  1361. comment=comment,
  1362. url=url,
  1363. user_id=user_obj.id,
  1364. token_id=token,
  1365. )
  1366. session.add(c_flag)
  1367. # Make sure we won't have SQLAlchemy error before we continue
  1368. session.flush()
  1369. if repo.settings.get("notify_on_commit_flag"):
  1370. pagure.lib.notify.notify_commit_flag(c_flag, username)
  1371. pagure.lib.notify.log(
  1372. repo,
  1373. topic="commit.flag.%s" % action,
  1374. msg=dict(
  1375. repo=repo.to_json(public=True),
  1376. flag=c_flag.to_json(public=True),
  1377. agent=user_obj.username,
  1378. ),
  1379. )
  1380. return ("Flag %s" % action, c_flag.uid)
  1381. def get_commit_flag(session, project, commit_hash):
  1382. """Return the commit flags corresponding to the specified git hash
  1383. (commitid) in the specified repository.
  1384. :arg session: the session with which to connect to the database
  1385. :arg repo: the pagure.lib.model.Project object corresponding to the
  1386. project whose commit has been flagged
  1387. :arg commit_hash: the hash of the commit who has been flagged
  1388. :return: list of pagure.lib.model.CommitFlag objects or an empty list
  1389. """
  1390. query = (
  1391. session.query(model.CommitFlag)
  1392. .filter(model.CommitFlag.project_id == project.id)
  1393. .filter(model.CommitFlag.commit_hash == commit_hash)
  1394. ).order_by(model.CommitFlag.date_updated)
  1395. return query.all()
  1396. def new_project(
  1397. session,
  1398. user,
  1399. name,
  1400. blacklist,
  1401. allowed_prefix,
  1402. repospanner_region,
  1403. description=None,
  1404. url=None,
  1405. avatar_email=None,
  1406. parent_id=None,
  1407. add_readme=False,
  1408. mirrored_from=None,
  1409. userobj=None,
  1410. prevent_40_chars=False,
  1411. namespace=None,
  1412. user_ns=False,
  1413. ignore_existing_repo=False,
  1414. private=False,
  1415. default_branch=None,
  1416. ):
  1417. """Create a new project based on the information provided.
  1418. Is an async operation, and returns task ID.
  1419. """
  1420. user_obj = get_user(session, user)
  1421. allowed_prefix = allowed_prefix + [grp for grp in user_obj.groups]
  1422. if user_ns:
  1423. allowed_prefix.append(user)
  1424. if not namespace:
  1425. namespace = user
  1426. if private:
  1427. allowed_prefix.append(user)
  1428. namespace = user
  1429. if namespace and namespace not in allowed_prefix:
  1430. raise pagure.exceptions.PagureException(
  1431. "The namespace of your project must be in the list of allowed "
  1432. "namespaces set by the admins of this pagure instance, or the "
  1433. "name of a group of which you are a member."
  1434. )
  1435. path = name if not namespace else "%s/%s" % (namespace, name)
  1436. matched = any(map(functools.partial(fnmatch.fnmatch, path), blacklist))
  1437. if matched:
  1438. raise pagure.exceptions.ProjectBlackListedException(
  1439. 'No project "%s" are allowed to be created due to potential '
  1440. "conflicts in URLs with pagure itself" % path
  1441. )
  1442. if len(name) == 40 and prevent_40_chars:
  1443. # We must block project with a name <foo>/<bar> where the length
  1444. # of <bar> is exactly 40 characters long as this would otherwise
  1445. # conflict with the old URL schema used for commit that was
  1446. # <foo>/<commit hash>. To keep backward compatibility, we have an
  1447. # endpoint redirecting <foo>/<commit hash> to <foo>/c/<commit hash>
  1448. # available as an option.
  1449. raise pagure.exceptions.PagureException(
  1450. "Your project name cannot have exactly 40 characters after "
  1451. "the `/`"
  1452. )
  1453. # Repo exists in the DB
  1454. repo = _get_project(session, name, namespace=namespace)
  1455. # this is leaking private repos but we're leaking them anyway if we fail
  1456. # to add the requested repo later. Let's be less clear about why :)
  1457. if repo:
  1458. raise pagure.exceptions.RepoExistsException(
  1459. 'It is not possible to create the repo "%s"' % (path)
  1460. )
  1461. if repospanner_region == "none":
  1462. repospanner_region = None
  1463. elif repospanner_region is None:
  1464. repospanner_region = pagure_config["REPOSPANNER_NEW_REPO"]
  1465. if (
  1466. repospanner_region
  1467. and repospanner_region not in pagure_config["REPOSPANNER_REGIONS"]
  1468. ):
  1469. raise Exception("repoSpanner region %s invalid" % repospanner_region)
  1470. project = model.Project(
  1471. name=name,
  1472. namespace=namespace,
  1473. repospanner_region=repospanner_region,
  1474. description=description if description else None,
  1475. url=url if url else None,
  1476. avatar_email=avatar_email if avatar_email else None,
  1477. user_id=user_obj.id,
  1478. parent_id=parent_id,
  1479. mirrored_from=mirrored_from,
  1480. private=private,
  1481. hook_token=pagure.lib.login.id_generator(40),
  1482. )
  1483. session.add(project)
  1484. # Flush so that a project ID is generated
  1485. session.flush()
  1486. for ltype in model.ProjectLock.lock_type.type.enums:
  1487. lock = model.ProjectLock(project_id=project.id, lock_type=ltype)
  1488. session.add(lock)
  1489. session.commit()
  1490. # Register creation et al
  1491. log_action(session, "created", project, user_obj)
  1492. pagure.lib.notify.log(
  1493. project,
  1494. topic="project.new",
  1495. msg=dict(
  1496. project=project.to_json(public=True), agent=user_obj.username
  1497. ),
  1498. )
  1499. return pagure.lib.tasks.create_project.delay(
  1500. user_obj.username,
  1501. namespace,
  1502. name,
  1503. add_readme,
  1504. ignore_existing_repo,
  1505. default_branch or pagure_config.get("GIT_DEFAULT_BRANCH"),
  1506. )
  1507. def new_issue(
  1508. session,
  1509. repo,
  1510. title,
  1511. content,
  1512. user,
  1513. issue_id=None,
  1514. issue_uid=None,
  1515. private=False,
  1516. related_prs=[],
  1517. status=None,
  1518. close_status=None,
  1519. notify=True,
  1520. date_created=None,
  1521. milestone=None,
  1522. priority=None,
  1523. assignee=None,
  1524. tags=None,
  1525. ):
  1526. """Create a new issue for the specified repo."""
  1527. user_obj = get_user(session, user)
  1528. # Only store the priority if there is one in the project
  1529. priorities = repo.priorities or []
  1530. try:
  1531. priority = int(priority)
  1532. except (ValueError, TypeError):
  1533. priority = None
  1534. if (
  1535. priorities
  1536. and priority is not None
  1537. and ("%s" % priority) not in priorities
  1538. ):
  1539. raise pagure.exceptions.PagureException(
  1540. "You are trying to create an issue with a priority that does "
  1541. "not exist in the project."
  1542. )
  1543. assignee_id = None
  1544. if assignee is not None:
  1545. assignee_id = get_user(session, assignee).id
  1546. issue = model.Issue(
  1547. id=issue_id or get_next_id(session, repo.id),
  1548. project_id=repo.id,
  1549. title=title,
  1550. content=content,
  1551. priority=priority,
  1552. milestone=milestone,
  1553. assignee_id=assignee_id,
  1554. user_id=user_obj.id,
  1555. uid=issue_uid or uuid.uuid4().hex,
  1556. private=private,
  1557. date_created=date_created,
  1558. )
  1559. if status is not None:
  1560. issue.status = status
  1561. if close_status is not None:
  1562. issue.close_status = close_status
  1563. issue.last_updated = datetime.datetime.utcnow()
  1564. session.add(issue)
  1565. # Make sure we won't have SQLAlchemy error before we create the issue
  1566. session.flush()
  1567. # Add the tags if any are specified
  1568. if tags is not None:
  1569. for lbl in tags:
  1570. tagobj = get_colored_tag(session, lbl, repo.id)
  1571. if not tagobj:
  1572. tagobj = model.TagColored(tag=lbl, project_id=repo.id)
  1573. session.add(tagobj)
  1574. session.flush()
  1575. dbobjtag = model.TagIssueColored(
  1576. issue_uid=issue.uid, tag_id=tagobj.id
  1577. )
  1578. session.add(dbobjtag)
  1579. session.commit()
  1580. pagure.lib.git.update_git(issue, repo=repo)
  1581. log_action(session, "created", issue, user_obj)
  1582. if notify:
  1583. pagure.lib.notify.notify_new_issue(issue, user=user_obj)
  1584. if not private:
  1585. pagure.lib.notify.log(
  1586. issue.project,
  1587. topic="issue.new",
  1588. msg=dict(
  1589. issue=issue.to_json(public=True),
  1590. project=issue.project.to_json(public=True),
  1591. agent=user_obj.username,
  1592. ),
  1593. )
  1594. return issue
  1595. def drop_issue(session, issue, user):
  1596. """Delete a specified issue."""
  1597. user_obj = get_user(session, user)
  1598. repotype = issue.repotype
  1599. uid = issue.uid
  1600. private = issue.private
  1601. session.delete(issue)
  1602. # Make sure we won't have SQLAlchemy error before we create the issue
  1603. session.flush()
  1604. if not private:
  1605. pagure.lib.notify.log(
  1606. issue.project,
  1607. topic="issue.drop",
  1608. msg=dict(
  1609. issue=issue.to_json(public=True),
  1610. project=issue.project.to_json(public=True),
  1611. agent=user_obj.username,
  1612. ),
  1613. )
  1614. session.commit()
  1615. pagure.lib.git.clean_git(issue.project, repotype, uid)
  1616. return issue
  1617. def new_pull_request(
  1618. session,
  1619. branch_from,
  1620. repo_to,
  1621. branch_to,
  1622. title,
  1623. user,
  1624. initial_comment=None,
  1625. allow_rebase=False,
  1626. repo_from=None,
  1627. remote_git=None,
  1628. requestuid=None,
  1629. requestid=None,
  1630. status="Open",
  1631. notify=True,
  1632. commit_start=None,
  1633. commit_stop=None,
  1634. ):
  1635. """Create a new pull request on the specified repo."""
  1636. if not repo_from and not remote_git:
  1637. raise pagure.exceptions.PagureException(
  1638. "Invalid input, you must specify either a local repo or a "
  1639. "remote one"
  1640. )
  1641. user_obj = get_user(session, user)
  1642. request = model.PullRequest(
  1643. id=requestid or get_next_id(session, repo_to.id),
  1644. uid=requestuid or uuid.uuid4().hex,
  1645. project_id=repo_to.id,
  1646. project_id_from=repo_from.id if repo_from else None,
  1647. remote_git=remote_git if remote_git else None,
  1648. branch=branch_to,
  1649. branch_from=branch_from,
  1650. title=title,
  1651. initial_comment=initial_comment or None,
  1652. allow_rebase=allow_rebase,
  1653. user_id=user_obj.id,
  1654. status=status,
  1655. commit_start=commit_start,
  1656. commit_stop=commit_stop,
  1657. )
  1658. request.last_updated = datetime.datetime.utcnow()
  1659. request.updated_on = datetime.datetime.utcnow()
  1660. session.add(request)
  1661. # Link the PR to issue(s) if there is such link
  1662. link_pr_to_issue_on_description(session, request)
  1663. # Make sure we won't have SQLAlchemy error before we create the request
  1664. session.flush()
  1665. pagure.lib.git.update_git(request, repo=request.project)
  1666. pagure.lib.tasks.link_pr_to_ticket.delay(request.uid)
  1667. log_action(session, "created", request, user_obj)
  1668. if notify:
  1669. pagure.lib.notify.notify_new_pull_request(request)
  1670. pagure.lib.notify.log(
  1671. request.project,
  1672. topic="pull-request.new",
  1673. msg=dict(
  1674. pullrequest=request.to_json(public=True), agent=user_obj.username
  1675. ),
  1676. )
  1677. # Send notification to the CI server
  1678. ci_hook = pagure.lib.plugins.get_plugin("Pagure CI")
  1679. ci_hook.db_object()
  1680. if (
  1681. pagure_config.get("PAGURE_CI_SERVICES")
  1682. and request.project.ci_hook
  1683. and request.project.ci_hook.active_pr
  1684. and not request.project.private
  1685. ):
  1686. pagure.lib.tasks_services.trigger_ci_build.delay(
  1687. pr_uid=request.uid,
  1688. cause=request.id,
  1689. branch=request.branch_from,
  1690. branch_to=request.branch,
  1691. ci_type=request.project.ci_hook.ci_type,
  1692. )
  1693. # Create the ref from the start
  1694. pagure.lib.tasks.sync_pull_ref.delay(
  1695. request.project.name,
  1696. request.project.namespace,
  1697. request.project.user.username if request.project.is_fork else None,
  1698. request.id,
  1699. )
  1700. return request
  1701. def link_pr_to_issue_on_description(session, request):
  1702. """Link the given request to issues it may be referring to in its
  1703. description if there is a description and such link in it.
  1704. """
  1705. _log.debug("Drop the existing relations")
  1706. # Drop the existing initial_comment_pr-based relations
  1707. session.query(model.PrToIssue).filter(
  1708. model.PrToIssue.pull_request_uid == request.uid
  1709. ).filter(model.PrToIssue.origin == "initial_comment_pr").delete(
  1710. synchronize_session="fetch"
  1711. )
  1712. # Rebuild the relations from the initial comment
  1713. if request.initial_comment:
  1714. _log.debug("Checking the initial comment for relations")
  1715. for line in request.initial_comment.split("\n"):
  1716. for issue in pagure.lib.link.get_relation(
  1717. session,
  1718. request.project.name,
  1719. request.project.user.user if request.project.is_fork else None,
  1720. request.project.namespace,
  1721. line,
  1722. "fixes",
  1723. include_prs=False,
  1724. ):
  1725. _log.debug(
  1726. "Link (fix) request %s to issue: %s (project: %s)"
  1727. % (request.id, issue.id, request.project.fullname)
  1728. )
  1729. pagure.lib.query.link_pr_issue(
  1730. session, issue, request, origin="initial_comment_pr"
  1731. )
  1732. for issue in pagure.lib.link.get_relation(
  1733. session,
  1734. request.project.name,
  1735. request.project.user.user if request.project.is_fork else None,
  1736. request.project.namespace,
  1737. line,
  1738. "relates",
  1739. include_prs=False,
  1740. ):
  1741. _log.debug(
  1742. "Link (relate) request %s to issue: %s (project: %s)"
  1743. % (request.id, issue.id, request.project.fullname)
  1744. )
  1745. pagure.lib.query.link_pr_issue(
  1746. session, issue, request, origin="initial_comment_pr"
  1747. )
  1748. else:
  1749. _log.debug("No initial comment, no need to continue")
  1750. def new_tag(session, tag_name, tag_description, tag_color, project_id):
  1751. """Return a new tag object"""
  1752. tagobj = model.TagColored(
  1753. tag=tag_name,
  1754. tag_description=tag_description,
  1755. tag_color=tag_color,
  1756. project_id=project_id,
  1757. )
  1758. session.add(tagobj)
  1759. session.flush()
  1760. return tagobj
  1761. def edit_issue(
  1762. session,
  1763. issue,
  1764. user,
  1765. repo=None,
  1766. title=None,
  1767. content=None,
  1768. status=None,
  1769. close_status=Unspecified,
  1770. priority=Unspecified,
  1771. milestone=Unspecified,
  1772. private=None,
  1773. ):
  1774. """Edit the specified issue.
  1775. :arg session: the session to use to connect to the database.
  1776. :arg issue: the pagure.lib.model.Issue object to edit.
  1777. :arg user: the username of the user editing the issue,
  1778. :kwarg repo: somehow this isn't used anywhere here...
  1779. :kwarg title: the new title of the issue if it's being changed
  1780. :kwarg content: the new content of the issue if it's being changed
  1781. :kwarg status: the new status of the issue if it's being changed
  1782. :kwarg close_status: the new close_status of the issue if it's being
  1783. changed
  1784. :kwarg priority: the new priority of the issue if it's being changed
  1785. :kwarg milestone: the new milestone of the issue if it's being changed
  1786. :kwarg private: the new private of the issue if it's being changed
  1787. """
  1788. user_obj = get_user(session, user)
  1789. if status and status != "Open" and issue.parents:
  1790. for parent in issue.parents:
  1791. if parent.status == "Open":
  1792. raise pagure.exceptions.PagureException(
  1793. "You cannot close a ticket that has ticket "
  1794. "depending that are still open."
  1795. )
  1796. edit = []
  1797. messages = []
  1798. if title and title != issue.title:
  1799. issue.title = title
  1800. edit.append("title")
  1801. if content and content != issue.content:
  1802. issue.content = content
  1803. edit.append("content")
  1804. if status and status != issue.status:
  1805. old_status = issue.status
  1806. issue.status = status
  1807. if status.lower() != "open":
  1808. issue.closed_at = datetime.datetime.utcnow()
  1809. issue.closed_by_id = user_obj.id
  1810. elif issue.close_status:
  1811. issue.close_status = None
  1812. close_status = Unspecified
  1813. edit.append("close_status")
  1814. edit.append("status")
  1815. messages.append(
  1816. "Issue status updated to: %s (was: %s)" % (status, old_status)
  1817. )
  1818. if close_status != Unspecified and close_status != issue.close_status:
  1819. old_status = issue.close_status
  1820. issue.close_status = close_status
  1821. edit.append("close_status")
  1822. msg = "Issue close_status updated to: %s" % close_status
  1823. if old_status:
  1824. msg += " (was: %s)" % old_status
  1825. if issue.status.lower() == "open" and close_status:
  1826. issue.status = "Closed"
  1827. issue.closed_at = datetime.datetime.utcnow()
  1828. edit.append("status")
  1829. messages.append(msg)
  1830. if priority != Unspecified:
  1831. priorities = issue.project.priorities
  1832. try:
  1833. priority = int(priority)
  1834. except (ValueError, TypeError):
  1835. priority = None
  1836. priority_string = "%s" % priority
  1837. if priority_string not in priorities:
  1838. priority = None
  1839. if priority != issue.priority:
  1840. old_priority = issue.priority
  1841. issue.priority = priority
  1842. edit.append("priority")
  1843. msg = "Issue priority set to: %s" % (
  1844. priorities[priority_string] if priority else None
  1845. )
  1846. if old_priority:
  1847. msg += " (was: %s)" % priorities.get(
  1848. "%s" % old_priority, old_priority
  1849. )
  1850. messages.append(msg)
  1851. if private in [True, False] and private != issue.private:
  1852. old_private = issue.private
  1853. issue.private = private
  1854. edit.append("private")
  1855. msg = "Issue private status set to: %s" % private
  1856. if old_private:
  1857. msg += " (was: %s)" % old_private
  1858. messages.append(msg)
  1859. if milestone != Unspecified and milestone != issue.milestone:
  1860. old_milestone = issue.milestone
  1861. issue.milestone = milestone
  1862. edit.append("milestone")
  1863. msg = "Issue set to the milestone: %s" % milestone
  1864. if old_milestone:
  1865. msg += " (was: %s)" % old_milestone
  1866. messages.append(msg)
  1867. issue.last_updated = datetime.datetime.utcnow()
  1868. # uniquify the list of edited fields
  1869. edit = list(set(edit))
  1870. pagure.lib.git.update_git(issue, repo=issue.project)
  1871. if "status" in edit:
  1872. log_action(session, issue.status.lower(), issue, user_obj)
  1873. pagure.lib.notify.notify_status_change_issue(issue, user_obj)
  1874. if not issue.private and edit:
  1875. pagure.lib.notify.log(
  1876. issue.project,
  1877. topic="issue.edit",
  1878. msg=dict(
  1879. issue=issue.to_json(public=True),
  1880. project=issue.project.to_json(public=True),
  1881. fields=sorted(set(edit)),
  1882. agent=user_obj.username,
  1883. ),
  1884. )
  1885. if REDIS and edit and not issue.project.private:
  1886. if issue.private:
  1887. REDIS.publish(
  1888. "pagure.%s" % issue.uid,
  1889. json.dumps({"issue": "private", "fields": edit}),
  1890. )
  1891. else:
  1892. REDIS.publish(
  1893. "pagure.%s" % issue.uid,
  1894. json.dumps(
  1895. {
  1896. "fields": edit,
  1897. "issue": issue.to_json(
  1898. public=True, with_comments=False
  1899. ),
  1900. "priorities": issue.project.priorities,
  1901. "content_updated": text2markdown(issue.content),
  1902. }
  1903. ),
  1904. )
  1905. if edit:
  1906. session.add(issue)
  1907. session.flush()
  1908. return messages
  1909. def update_project_settings(session, repo, settings, user, from_api=False):
  1910. """Update the settings of a project.
  1911. If from_api is true, all values that are not specified will be changed
  1912. back to their default value.
  1913. Otherwise, if from_api is False, all non-specified values are assumed
  1914. to be set to ``False`` or ``None``.
  1915. """
  1916. user_obj = get_user(session, user)
  1917. update = []
  1918. new_settings = repo.settings
  1919. for key in new_settings:
  1920. if key in settings:
  1921. if key == "Minimum_score_to_merge_pull-request":
  1922. try:
  1923. settings[key] = int(settings[key]) if settings[key] else -1
  1924. except (ValueError, TypeError):
  1925. raise pagure.exceptions.PagureException(
  1926. "Please enter a numeric value for the 'minimum "
  1927. "score to merge pull request' field."
  1928. )
  1929. elif key == "Web-hooks":
  1930. settings[key] = settings[key] or None
  1931. else:
  1932. # All the remaining keys are boolean, so True is provided
  1933. # as 'y' by the html, let's convert it back
  1934. settings[key] = settings[key] in ["y", True]
  1935. if new_settings[key] != settings[key]:
  1936. update.append(key)
  1937. new_settings[key] = settings[key]
  1938. else:
  1939. if from_api:
  1940. val = new_settings[key]
  1941. else:
  1942. val = False
  1943. if key == "Web-hooks":
  1944. val = None
  1945. # Ensure the default value is different from what is stored.
  1946. if new_settings[key] != val:
  1947. update.append(key)
  1948. new_settings[key] = val
  1949. if not update:
  1950. return "No settings to change"
  1951. else:
  1952. repo.settings = new_settings
  1953. repo.date_modified = datetime.datetime.utcnow()
  1954. session.add(repo)
  1955. session.flush()
  1956. pagure.lib.notify.log(
  1957. repo,
  1958. topic="project.edit",
  1959. msg=dict(
  1960. project=repo.to_json(public=True),
  1961. fields=sorted(update),
  1962. agent=user_obj.username,
  1963. ),
  1964. )
  1965. if "pull_request_access_only" in update:
  1966. update_read_only_mode(session, repo, read_only=True)
  1967. session.add(repo)
  1968. session.flush()
  1969. pagure.lib.git.generate_gitolite_acls(project=repo)
  1970. return "Edited successfully settings of repo: %s" % repo.fullname
  1971. def update_user_settings(session, settings, user):
  1972. """Update the settings of a project."""
  1973. user_obj = get_user(session, user)
  1974. update = []
  1975. new_settings = user_obj.settings
  1976. for key in new_settings:
  1977. if key in settings:
  1978. if new_settings[key] != settings[key]:
  1979. update.append(key)
  1980. new_settings[key] = settings[key]
  1981. else:
  1982. if new_settings[key] is not False:
  1983. update.append(key)
  1984. new_settings[key] = False
  1985. if not update:
  1986. return "No settings to change"
  1987. else:
  1988. user_obj.settings = new_settings
  1989. session.add(user_obj)
  1990. session.flush()
  1991. return "Successfully edited your settings"
  1992. def fork_project(session, user, repo, editbranch=None, editfile=None):
  1993. """Fork a given project into the user's forks."""
  1994. if _get_project(session, repo.name, user, repo.namespace) is not None:
  1995. raise pagure.exceptions.RepoExistsException(
  1996. 'Repo "forks/%s/%s" already exists' % (user, repo.name)
  1997. )
  1998. user_obj = get_user(session, user)
  1999. fork_repospanner_setting = pagure_config["REPOSPANNER_NEW_FORK"]
  2000. if fork_repospanner_setting is None:
  2001. repospanner_region = None
  2002. elif fork_repospanner_setting is True:
  2003. repospanner_region = repo.repospanner_region
  2004. else:
  2005. repospanner_region = fork_repospanner_setting
  2006. project = model.Project(
  2007. name=repo.name,
  2008. namespace=repo.namespace,
  2009. description=repo.description,
  2010. repospanner_region=repospanner_region,
  2011. private=repo.private,
  2012. user_id=user_obj.id,
  2013. parent_id=repo.id,
  2014. is_fork=True,
  2015. hook_token=pagure.lib.login.id_generator(40),
  2016. )
  2017. # disable issues, PRs in the fork by default
  2018. default_repo_settings = project.settings
  2019. default_repo_settings["issue_tracker"] = False
  2020. default_repo_settings["pull_requests"] = False
  2021. project.settings = default_repo_settings
  2022. session.add(project)
  2023. # Make sure we won't have SQLAlchemy error before we create the repo
  2024. session.flush()
  2025. session.commit()
  2026. task = pagure.lib.tasks.fork.delay(
  2027. repo.name,
  2028. repo.namespace,
  2029. repo.user.username if repo.is_fork else None,
  2030. user,
  2031. editbranch,
  2032. editfile,
  2033. )
  2034. return task
  2035. def search_projects(
  2036. session,
  2037. username=None,
  2038. fork=None,
  2039. tags=None,
  2040. namespace=None,
  2041. pattern=None,
  2042. start=None,
  2043. limit=None,
  2044. count=False,
  2045. sort=None,
  2046. exclude_groups=None,
  2047. private=None,
  2048. owner=None,
  2049. ):
  2050. """List existing projects"""
  2051. projects = session.query(sqlalchemy.distinct(model.Project.id))
  2052. if owner is not None and username is not None:
  2053. raise RuntimeError(
  2054. "You cannot supply both a username and an owner "
  2055. "as parameters in the `search_projects` function"
  2056. )
  2057. elif owner is not None:
  2058. if owner.startswith("!"):
  2059. projects = projects.join(model.User).filter(
  2060. model.User.user != owner[1:]
  2061. )
  2062. else:
  2063. projects = projects.join(model.User).filter(
  2064. model.User.user == owner
  2065. )
  2066. elif username is not None:
  2067. projects = projects.filter(
  2068. # User created the project
  2069. sqlalchemy.and_(
  2070. model.User.user == username,
  2071. model.User.id == model.Project.user_id,
  2072. )
  2073. )
  2074. sub_q2 = session.query(model.Project.id).filter(
  2075. # User got admin or commit right
  2076. sqlalchemy.and_(
  2077. model.User.user == username,
  2078. model.User.id == model.ProjectUser.user_id,
  2079. model.ProjectUser.project_id == model.Project.id,
  2080. sqlalchemy.or_(
  2081. model.ProjectUser.access == "admin",
  2082. model.ProjectUser.access == "commit",
  2083. model.ProjectUser.access == "collaborator",
  2084. ),
  2085. )
  2086. )
  2087. sub_q3 = session.query(model.Project.id).filter(
  2088. # User created a group that has admin or commit right
  2089. sqlalchemy.and_(
  2090. model.User.user == username,
  2091. model.PagureGroup.user_id == model.User.id,
  2092. model.PagureGroup.group_type == "user",
  2093. model.PagureGroup.id == model.ProjectGroup.group_id,
  2094. model.Project.id == model.ProjectGroup.project_id,
  2095. sqlalchemy.or_(
  2096. model.ProjectGroup.access == "admin",
  2097. model.ProjectGroup.access == "commit",
  2098. model.ProjectGroup.access == "collaborator",
  2099. ),
  2100. )
  2101. )
  2102. sub_q4 = session.query(model.Project.id).filter(
  2103. # User is part of a group that has admin or commit right
  2104. sqlalchemy.and_(
  2105. model.User.user == username,
  2106. model.PagureUserGroup.user_id == model.User.id,
  2107. model.PagureUserGroup.group_id == model.PagureGroup.id,
  2108. model.PagureGroup.group_type == "user",
  2109. model.PagureGroup.id == model.ProjectGroup.group_id,
  2110. model.Project.id == model.ProjectGroup.project_id,
  2111. sqlalchemy.or_(
  2112. model.ProjectGroup.access == "admin",
  2113. model.ProjectGroup.access == "commit",
  2114. model.ProjectGroup.access == "collaborator",
  2115. ),
  2116. )
  2117. )
  2118. # Exclude projects that the user has accessed via a group that we
  2119. # do not want to include
  2120. if exclude_groups:
  2121. sub_q3 = sub_q3.filter(
  2122. model.PagureGroup.group_name.notin_(exclude_groups)
  2123. )
  2124. sub_q4 = sub_q4.filter(
  2125. model.PagureGroup.group_name.notin_(exclude_groups)
  2126. )
  2127. projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
  2128. if not private:
  2129. projects = projects.filter(
  2130. model.Project.private == False # noqa: E712
  2131. )
  2132. # No filtering is done if private == username i.e if the owner of the
  2133. # project is viewing the project
  2134. elif isinstance(private, six.string_types) and private != username:
  2135. # All the public repo
  2136. subquery0 = session.query(
  2137. sqlalchemy.distinct(model.Project.id)
  2138. ).filter(
  2139. model.Project.private == False # noqa: E712
  2140. )
  2141. sub_q1 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  2142. sqlalchemy.and_(
  2143. model.Project.private == True, # noqa: E712
  2144. model.User.id == model.Project.user_id,
  2145. model.User.user == private,
  2146. )
  2147. )
  2148. sub_q2 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  2149. # User got admin or commit right
  2150. sqlalchemy.and_(
  2151. model.Project.private == True, # noqa: E712
  2152. model.User.user == private,
  2153. model.User.id == model.ProjectUser.user_id,
  2154. model.ProjectUser.project_id == model.Project.id,
  2155. sqlalchemy.or_(
  2156. model.ProjectUser.access == "admin",
  2157. model.ProjectUser.access == "commit",
  2158. model.ProjectUser.access == "collaborator",
  2159. ),
  2160. )
  2161. )
  2162. sub_q3 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  2163. # User created a group that has admin or commit right
  2164. sqlalchemy.and_(
  2165. model.Project.private == True, # noqa: E712
  2166. model.User.user == private,
  2167. model.PagureGroup.user_id == model.User.id,
  2168. model.PagureGroup.group_type == "user",
  2169. model.PagureGroup.id == model.ProjectGroup.group_id,
  2170. model.Project.id == model.ProjectGroup.project_id,
  2171. sqlalchemy.or_(
  2172. model.ProjectGroup.access == "admin",
  2173. model.ProjectGroup.access == "commit",
  2174. model.ProjectGroup.access == "collaborator",
  2175. ),
  2176. )
  2177. )
  2178. sub_q4 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  2179. # User is part of a group that has admin or commit right
  2180. sqlalchemy.and_(
  2181. model.Project.private == True, # noqa: E712
  2182. model.User.user == private,
  2183. model.PagureUserGroup.user_id == model.User.id,
  2184. model.PagureUserGroup.group_id == model.PagureGroup.id,
  2185. model.PagureGroup.group_type == "user",
  2186. model.PagureGroup.id == model.ProjectGroup.group_id,
  2187. model.Project.id == model.ProjectGroup.project_id,
  2188. sqlalchemy.or_(
  2189. model.ProjectGroup.access == "admin",
  2190. model.ProjectGroup.access == "commit",
  2191. model.ProjectGroup.access == "collaborator",
  2192. ),
  2193. )
  2194. )
  2195. # Exclude projects that the user has accessed via a group that we
  2196. # do not want to include
  2197. if exclude_groups:
  2198. sub_q3 = sub_q3.filter(
  2199. model.PagureGroup.group_name.notin_(exclude_groups)
  2200. )
  2201. sub_q4 = sub_q4.filter(
  2202. model.PagureGroup.group_name.notin_(exclude_groups)
  2203. )
  2204. private_repo_subq = (
  2205. subquery0.union(sub_q1).union(sub_q2).union(sub_q3).union(sub_q4)
  2206. )
  2207. # There is something going on here, we shouldn't have to invoke/call
  2208. # the sub-query, it should work fine with:
  2209. # model.Project.id.in_(private_repo_subq.subquery())
  2210. # however, it does not. Either something gets really confused with
  2211. # sqlite or the generated SQL is broken
  2212. # The issues seems to be with the unions in the subquery just above.
  2213. # Since we can't quite get this to work, let's bite the bullet and go
  2214. # with this approach, but damn I don't like it!
  2215. # This issue appeared with sqlalchemy 1.3.0 and is still present in
  2216. # 1.3.13 tested today.
  2217. private_repos = private_repo_subq.all()
  2218. if private_repos:
  2219. projects = projects.filter(
  2220. model.Project.id.in_(list(set(zip(*private_repos)))[0])
  2221. )
  2222. if fork is not None:
  2223. if fork is True:
  2224. projects = projects.filter(
  2225. model.Project.is_fork == True # noqa: E712
  2226. )
  2227. elif fork is False:
  2228. projects = projects.filter(
  2229. model.Project.is_fork == False # noqa: E712
  2230. )
  2231. if tags:
  2232. if not isinstance(tags, (list, tuple)):
  2233. tags = [tags]
  2234. projects = projects.filter(
  2235. model.Project.id == model.TagProject.project_id
  2236. ).filter(model.TagProject.tag.in_(tags))
  2237. if pattern:
  2238. pattern = pattern.replace("*", "%")
  2239. if "%" in pattern:
  2240. projects = projects.filter(model.Project.name.ilike(pattern))
  2241. else:
  2242. projects = projects.filter(model.Project.name == pattern)
  2243. if namespace:
  2244. projects = projects.filter(model.Project.namespace == namespace)
  2245. query = session.query(model.Project).filter(
  2246. model.Project.id.in_(projects.as_scalar())
  2247. )
  2248. if sort == "latest":
  2249. query = query.order_by(model.Project.date_created.desc())
  2250. elif sort == "oldest":
  2251. query = query.order_by(model.Project.date_created.asc())
  2252. else:
  2253. query = query.order_by(asc(func.lower(model.Project.name)))
  2254. if start is not None:
  2255. query = query.offset(start)
  2256. if limit is not None:
  2257. query = query.limit(limit)
  2258. if count:
  2259. return query.count()
  2260. else:
  2261. return query.all()
  2262. def list_users_projects(
  2263. session,
  2264. username,
  2265. fork=None,
  2266. tags=None,
  2267. namespace=None,
  2268. pattern=None,
  2269. start=None,
  2270. limit=None,
  2271. count=False,
  2272. sort=None,
  2273. exclude_groups=None,
  2274. private=None,
  2275. acls=None,
  2276. ):
  2277. """List a users projects"""
  2278. projects = session.query(sqlalchemy.distinct(model.Project.id))
  2279. if acls is None:
  2280. acls = ["main admin", "admin", "collaborator", "commit", "ticket"]
  2281. if username is not None:
  2282. projects = projects.filter(
  2283. # User created the project
  2284. sqlalchemy.and_(
  2285. model.User.user == username,
  2286. model.User.id == model.Project.user_id,
  2287. )
  2288. )
  2289. if "main admin" not in acls:
  2290. projects = projects.filter(model.User.id != model.Project.user_id)
  2291. sub_q2 = session.query(model.Project.id).filter(
  2292. # User got admin or commit right
  2293. sqlalchemy.and_(
  2294. model.User.user == username,
  2295. model.User.id == model.ProjectUser.user_id,
  2296. model.ProjectUser.project_id == model.Project.id,
  2297. model.ProjectUser.access.in_(acls),
  2298. )
  2299. )
  2300. sub_q3 = session.query(model.Project.id).filter(
  2301. # User created a group that has admin or commit right
  2302. sqlalchemy.and_(
  2303. model.User.user == username,
  2304. model.PagureGroup.user_id == model.User.id,
  2305. model.PagureGroup.group_type == "user",
  2306. model.PagureGroup.id == model.ProjectGroup.group_id,
  2307. model.Project.id == model.ProjectGroup.project_id,
  2308. model.ProjectGroup.access.in_(acls),
  2309. )
  2310. )
  2311. sub_q4 = session.query(model.Project.id).filter(
  2312. # User is part of a group that has admin or commit right
  2313. sqlalchemy.and_(
  2314. model.User.user == username,
  2315. model.PagureUserGroup.user_id == model.User.id,
  2316. model.PagureUserGroup.group_id == model.PagureGroup.id,
  2317. model.PagureGroup.group_type == "user",
  2318. model.PagureGroup.id == model.ProjectGroup.group_id,
  2319. model.Project.id == model.ProjectGroup.project_id,
  2320. model.ProjectGroup.access.in_(acls),
  2321. )
  2322. )
  2323. # Exclude projects that the user has accessed via a group that we
  2324. # do not want to include
  2325. if exclude_groups:
  2326. sub_q3 = sub_q3.filter(
  2327. model.PagureGroup.group_name.notin_(exclude_groups)
  2328. )
  2329. sub_q4 = sub_q4.filter(
  2330. model.PagureGroup.group_name.notin_(exclude_groups)
  2331. )
  2332. projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
  2333. if not private:
  2334. projects = projects.filter(
  2335. model.Project.private == False # noqa: E712
  2336. )
  2337. # No filtering is done if private == username i.e if the owner of the
  2338. # project is viewing the project
  2339. elif isinstance(private, six.string_types) and private != username:
  2340. # All the public repo
  2341. subquery0 = session.query(
  2342. sqlalchemy.distinct(model.Project.id)
  2343. ).filter(
  2344. model.Project.private == False # noqa: E712
  2345. )
  2346. sub_q1 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  2347. sqlalchemy.and_(
  2348. model.Project.private == True, # noqa: E712
  2349. model.User.id == model.Project.user_id,
  2350. model.User.user == private,
  2351. )
  2352. )
  2353. sub_q2 = session.query(model.Project.id).filter(
  2354. # User got admin or commit right
  2355. sqlalchemy.and_(
  2356. model.Project.private == True, # noqa: E712
  2357. model.User.user == private,
  2358. model.User.id == model.ProjectUser.user_id,
  2359. model.ProjectUser.project_id == model.Project.id,
  2360. model.ProjectUser.access.in_(acls),
  2361. )
  2362. )
  2363. sub_q3 = session.query(model.Project.id).filter(
  2364. # User created a group that has admin or commit right
  2365. sqlalchemy.and_(
  2366. model.Project.private == True, # noqa: E712
  2367. model.User.user == private,
  2368. model.PagureGroup.user_id == model.User.id,
  2369. model.PagureGroup.group_type == "user",
  2370. model.PagureGroup.id == model.ProjectGroup.group_id,
  2371. model.Project.id == model.ProjectGroup.project_id,
  2372. model.ProjectGroup.access.in_(acls),
  2373. )
  2374. )
  2375. sub_q4 = session.query(model.Project.id).filter(
  2376. # User is part of a group that has admin or commit right
  2377. sqlalchemy.and_(
  2378. model.Project.private == True, # noqa: E712
  2379. model.User.user == private,
  2380. model.PagureUserGroup.user_id == model.User.id,
  2381. model.PagureUserGroup.group_id == model.PagureGroup.id,
  2382. model.PagureGroup.group_type == "user",
  2383. model.PagureGroup.id == model.ProjectGroup.group_id,
  2384. model.Project.id == model.ProjectGroup.project_id,
  2385. model.ProjectGroup.access.in_(acls),
  2386. )
  2387. )
  2388. # Exclude projects that the user has accessed via a group that we
  2389. # do not want to include
  2390. if exclude_groups:
  2391. sub_q3 = sub_q3.filter(
  2392. model.PagureGroup.group_name.notin_(exclude_groups)
  2393. )
  2394. sub_q4 = sub_q4.filter(
  2395. model.PagureGroup.group_name.notin_(exclude_groups)
  2396. )
  2397. projects = projects.filter(
  2398. model.Project.id.in_(
  2399. subquery0.union(sub_q1)
  2400. .union(sub_q2)
  2401. .union(sub_q3)
  2402. .union(sub_q4)
  2403. )
  2404. )
  2405. if fork is not None:
  2406. if fork is True:
  2407. projects = projects.filter(
  2408. model.Project.is_fork == True # noqa: E712
  2409. )
  2410. elif fork is False:
  2411. projects = projects.filter(
  2412. model.Project.is_fork == False # noqa: E712
  2413. )
  2414. if tags:
  2415. if not isinstance(tags, (list, tuple)):
  2416. tags = [tags]
  2417. projects = projects.filter(
  2418. model.Project.id == model.TagProject.project_id
  2419. ).filter(model.TagProject.tag.in_(tags))
  2420. if pattern:
  2421. pattern = pattern.replace("*", "%")
  2422. if "%" in pattern:
  2423. projects = projects.filter(model.Project.name.ilike(pattern))
  2424. else:
  2425. projects = projects.filter(model.Project.name == pattern)
  2426. if namespace:
  2427. projects = projects.filter(model.Project.namespace == namespace)
  2428. query = session.query(model.Project).filter(
  2429. model.Project.id.in_(projects.subquery())
  2430. )
  2431. if sort == "latest":
  2432. query = query.order_by(model.Project.date_created.desc())
  2433. elif sort == "oldest":
  2434. query = query.order_by(model.Project.date_created.asc())
  2435. else:
  2436. query = query.order_by(asc(func.lower(model.Project.name)))
  2437. if start is not None:
  2438. query = query.offset(start)
  2439. if limit is not None:
  2440. query = query.limit(limit)
  2441. if count:
  2442. return query.count()
  2443. else:
  2444. return query.all()
  2445. def _get_project(session, name, user=None, namespace=None):
  2446. """Get a project from the database"""
  2447. case = pagure_config.get("CASE_SENSITIVE", False)
  2448. query = session.query(model.Project)
  2449. if not case:
  2450. query = query.filter(func.lower(model.Project.name) == name.lower())
  2451. else:
  2452. query = query.filter(model.Project.name == name)
  2453. if namespace:
  2454. if not case:
  2455. query = query.filter(
  2456. func.lower(model.Project.namespace) == namespace.lower()
  2457. )
  2458. else:
  2459. query = query.filter(model.Project.namespace == namespace)
  2460. else:
  2461. query = query.filter(model.Project.namespace == namespace)
  2462. if user is not None:
  2463. query = (
  2464. query.filter(model.User.user == user)
  2465. .filter(model.User.id == model.Project.user_id)
  2466. .filter(model.Project.is_fork == True) # noqa: E712
  2467. )
  2468. else:
  2469. query = query.filter(model.Project.is_fork == False) # noqa: E712
  2470. try:
  2471. return query.one()
  2472. except sqlalchemy.orm.exc.NoResultFound:
  2473. return None
  2474. def search_issues(
  2475. session,
  2476. repo=None,
  2477. issueid=None,
  2478. issueuid=None,
  2479. status=None,
  2480. closed=False,
  2481. tags=None,
  2482. assignee=None,
  2483. author=None,
  2484. private=None,
  2485. priority=None,
  2486. milestones=None,
  2487. count=False,
  2488. offset=None,
  2489. limit=None,
  2490. search_id=None,
  2491. search_pattern=None,
  2492. search_content=None,
  2493. custom_search=None,
  2494. updated_after=None,
  2495. no_milestones=None,
  2496. created_since=None,
  2497. created_until=None,
  2498. updated_since=None,
  2499. updated_until=None,
  2500. closed_since=None,
  2501. closed_until=None,
  2502. order="desc",
  2503. order_key=None,
  2504. ):
  2505. """Retrieve one or more issues associated to a project with the given
  2506. criterias.
  2507. Watch out that the closed argument is incompatible with the status
  2508. argument. The closed argument will return all the issues whose status
  2509. is not 'Open', otherwise it will return the issues having the specified
  2510. status.
  2511. The `tags` argument can be used to filter the issues returned based on
  2512. a certain tag.
  2513. If the `issueid` argument is specified a single Issue object (or None)
  2514. will be returned instead of a list of Issue objects.
  2515. :arg session: the session to use to connect to the database.
  2516. :arg repo: a Project object to which the issues should be associated
  2517. :type repo: pagure.lib.model.Project
  2518. :kwarg issueid: the identifier of the issue to look for
  2519. :type issueid: int or None
  2520. :kwarg issueuid: the unique identifier of the issue to look for
  2521. :type issueuid: str or None
  2522. :kwarg status: the status of the issue to look for (incompatible with
  2523. the `closed` argument).
  2524. :type status: str or None
  2525. :kwarg closed: a boolean indicating whether the issue to retrieve are
  2526. closed or open (incompatible with the `status` argument).
  2527. :type closed: bool or None
  2528. :kwarg tags: a tag the issue(s) returned should be associated with
  2529. :type tags: str or list(str) or None
  2530. :kwarg assignee: the name of the user assigned to the issues to search
  2531. :type assignee: str or None
  2532. :kwarg author: the name of the user who created the issues to search
  2533. :type author: str or None
  2534. :kwarg private: boolean or string to use to include or exclude private
  2535. tickets. Defaults to False.
  2536. If False: private tickets are excluded
  2537. If None: private tickets are included
  2538. If user name is specified: private tickets reported by that user
  2539. are included.
  2540. :type private: False, None or str
  2541. :kwarg priority: the priority of the issues to search
  2542. :type priority: int or None
  2543. :kwarg milestones: a milestone the issue(s) returned should be
  2544. associated with.
  2545. :type milestones: str or list(str) or None
  2546. :kwarg count: a boolean to specify if the method should return the list
  2547. of Issues or just do a COUNT query.
  2548. :type count: boolean
  2549. :kwarg search_id: an integer to search in issues identifier
  2550. :type search_id: int or None
  2551. :kwarg search_pattern: a string to search in issues title
  2552. :type search_pattern: str or None
  2553. :kwarg search_content: a string to search in the issues comments
  2554. :type search_content: str or None
  2555. :kwarg custom_search: a dictionary of key/values to be used when
  2556. searching issues with a custom key constraint
  2557. :type custom_search: dict or None
  2558. :kwarg updated_after: datetime's date format (e.g. 2016-11-15) used to
  2559. filter issues updated after that date
  2560. :type updated_after: str or None
  2561. :kwarg no_milestones: Request issues that do not have a milestone set yet
  2562. :type None, True, or False
  2563. :kwarg order: Order issues in 'asc' or 'desc' order.
  2564. :type order: None, str
  2565. :kwarg order_key: Order issues by database column
  2566. :type order_key: None, str
  2567. :return: A single Issue object if issueid is specified, a list of Project
  2568. objects otherwise.
  2569. :rtype: Project or [Project]
  2570. """
  2571. query = session.query(sqlalchemy.distinct(model.Issue.uid))
  2572. if repo is not None:
  2573. query = query.filter(model.Issue.project_id == repo.id)
  2574. if updated_after:
  2575. query = query.filter(model.Issue.last_updated >= updated_after)
  2576. if issueid is not None:
  2577. query = query.filter(model.Issue.id == issueid)
  2578. if issueuid is not None:
  2579. query = query.filter(model.Issue.uid == issueuid)
  2580. if status is not None:
  2581. if status in ["Open", "Closed"]:
  2582. query = query.filter(model.Issue.status == status)
  2583. else:
  2584. query = query.filter(model.Issue.close_status == status)
  2585. if closed:
  2586. query = query.filter(model.Issue.status != "Open")
  2587. if priority:
  2588. query = query.filter(model.Issue.priority == priority)
  2589. if tags is not None and tags != []:
  2590. if isinstance(tags, six.string_types):
  2591. tags = [tags]
  2592. notags = []
  2593. ytags = []
  2594. for tag in tags:
  2595. if tag.startswith("!"):
  2596. notags.append(tag[1:])
  2597. else:
  2598. ytags.append(tag)
  2599. if ytags:
  2600. sub_q2 = session.query(sqlalchemy.distinct(model.Issue.uid))
  2601. if repo is not None:
  2602. sub_q2 = sub_q2.filter(model.Issue.project_id == repo.id)
  2603. sub_q2 = (
  2604. sub_q2.filter(
  2605. model.Issue.uid == model.TagIssueColored.issue_uid
  2606. )
  2607. .filter(model.TagIssueColored.tag_id == model.TagColored.id)
  2608. .filter(model.TagColored.tag.in_(ytags))
  2609. )
  2610. if notags:
  2611. sub_q3 = session.query(sqlalchemy.distinct(model.Issue.uid))
  2612. if repo is not None:
  2613. sub_q3 = sub_q3.filter(model.Issue.project_id == repo.id)
  2614. sub_q3 = (
  2615. sub_q3.filter(
  2616. model.Issue.uid == model.TagIssueColored.issue_uid
  2617. )
  2618. .filter(model.TagIssueColored.tag_id == model.TagColored.id)
  2619. .filter(model.TagColored.tag.in_(notags))
  2620. )
  2621. # Adjust the main query based on the parameters specified
  2622. if ytags and not notags:
  2623. query = query.filter(model.Issue.uid.in_(sub_q2))
  2624. elif not ytags and notags:
  2625. query = query.filter(~model.Issue.uid.in_(sub_q3))
  2626. elif ytags and notags:
  2627. final_set = set([i[0] for i in sub_q2.all()]) - set(
  2628. [i[0] for i in sub_q3.all()]
  2629. )
  2630. if final_set:
  2631. query = query.filter(model.Issue.uid.in_(list(final_set)))
  2632. if assignee is not None:
  2633. assignee = "%s" % assignee
  2634. if not pagure.utils.is_true(assignee, ["false", "0", "true", "1"]):
  2635. reverseassignee = False
  2636. if assignee.startswith("!"):
  2637. reverseassignee = True
  2638. assignee = assignee[1:]
  2639. userassignee = (
  2640. session.query(model.User.id)
  2641. .filter(model.User.user == assignee)
  2642. .subquery()
  2643. )
  2644. if reverseassignee:
  2645. sub = session.query(model.Issue.uid).filter(
  2646. model.Issue.assignee_id == userassignee
  2647. )
  2648. query = query.filter(~model.Issue.uid.in_(sub))
  2649. else:
  2650. query = query.filter(model.Issue.assignee_id == userassignee)
  2651. elif pagure.utils.is_true(assignee):
  2652. query = query.filter(model.Issue.assignee_id.isnot(None))
  2653. else:
  2654. query = query.filter(model.Issue.assignee_id.is_(None))
  2655. if author is not None:
  2656. userauthor = (
  2657. session.query(model.User.id)
  2658. .filter(model.User.user == author)
  2659. .subquery()
  2660. )
  2661. query = query.filter(model.Issue.user_id == userauthor)
  2662. if private is False:
  2663. query = query.filter(model.Issue.private == False) # noqa: E712
  2664. elif isinstance(private, six.string_types):
  2665. userprivate = (
  2666. session.query(model.User.id)
  2667. .filter(model.User.user == private)
  2668. .subquery()
  2669. )
  2670. query = query.filter(
  2671. sqlalchemy.or_(
  2672. model.Issue.private == False, # noqa: E712
  2673. sqlalchemy.and_(
  2674. model.Issue.private == True, # noqa: E712
  2675. model.Issue.user_id == userprivate,
  2676. ),
  2677. sqlalchemy.and_(
  2678. model.Issue.private == True, # noqa: E712
  2679. model.Issue.assignee_id == userprivate,
  2680. ),
  2681. )
  2682. )
  2683. if no_milestones and milestones is not None and milestones != []:
  2684. # Asking for issues with no milestone or a specific milestone
  2685. if isinstance(milestones, six.string_types):
  2686. milestones = [milestones]
  2687. query = query.filter(
  2688. (model.Issue.milestone.is_(None))
  2689. | (model.Issue.milestone.in_(milestones))
  2690. )
  2691. elif no_milestones:
  2692. # Asking for issues without a milestone
  2693. query = query.filter(model.Issue.milestone.is_(None))
  2694. elif milestones is not None and milestones != []:
  2695. # Asking for a single specific milestone
  2696. if isinstance(milestones, six.string_types):
  2697. milestones = [milestones]
  2698. query = query.filter(model.Issue.milestone.in_(milestones))
  2699. elif no_milestones is False:
  2700. # Asking for all ticket with a milestone
  2701. query = query.filter(model.Issue.milestone.isnot(None))
  2702. if created_since:
  2703. query = query.filter(model.Issue.date_created >= created_since)
  2704. if created_until:
  2705. query = query.filter(model.Issue.date_created <= created_until)
  2706. if updated_since:
  2707. query = query.filter(model.Issue.last_updated <= updated_since)
  2708. if updated_until:
  2709. query = query.filter(model.Issue.last_updated <= updated_until)
  2710. if closed_since:
  2711. query = query.filter(model.Issue.closed_at <= closed_since)
  2712. if closed_until:
  2713. query = query.filter(model.Issue.closed_at <= closed_until)
  2714. if custom_search:
  2715. constraints = []
  2716. for key in custom_search:
  2717. value = custom_search[key]
  2718. if "*" in value:
  2719. value = value.replace("*", "%")
  2720. constraints.append(
  2721. sqlalchemy.and_(
  2722. model.IssueKeys.name == key,
  2723. model.IssueValues.value.ilike(value),
  2724. )
  2725. )
  2726. else:
  2727. constraints.append(
  2728. sqlalchemy.and_(
  2729. model.IssueKeys.name == key,
  2730. model.IssueValues.value == value,
  2731. )
  2732. )
  2733. if constraints:
  2734. query = query.filter(
  2735. model.Issue.uid == model.IssueValues.issue_uid
  2736. ).filter(model.IssueValues.key_id == model.IssueKeys.id)
  2737. query = query.filter(
  2738. sqlalchemy.or_((const for const in constraints))
  2739. )
  2740. if search_content is not None:
  2741. query = query.outerjoin(model.IssueComment).filter(
  2742. sqlalchemy.or_(
  2743. model.Issue.content.ilike("%%%s%%" % search_content),
  2744. sqlalchemy.and_(
  2745. model.Issue.uid == model.IssueComment.issue_uid,
  2746. model.IssueComment.comment.ilike(
  2747. "%%%s%%" % search_content
  2748. ),
  2749. ),
  2750. )
  2751. )
  2752. query = session.query(model.Issue).filter(
  2753. model.Issue.uid.in_(query.subquery())
  2754. )
  2755. if repo is not None:
  2756. query = query.filter(model.Issue.project_id == repo.id)
  2757. if search_pattern is not None:
  2758. query = query.filter(
  2759. model.Issue.title.ilike("%%%s%%" % search_pattern)
  2760. )
  2761. if search_id is not None:
  2762. query = query.filter(
  2763. cast(model.Issue.id, Text).ilike("%%%s%%" % search_id)
  2764. )
  2765. column = model.Issue.date_created
  2766. if order_key:
  2767. # If we are ordering by assignee, then order by the assignees'
  2768. # usernames
  2769. if order_key == "assignee":
  2770. # We must do a LEFT JOIN on model.Issue.assignee because there are
  2771. # two foreign keys on model.Issue tied to model.User. This tells
  2772. # SQLAlchemy which foreign key on model.User to order on.
  2773. query = query.outerjoin(
  2774. model.User, model.Issue.assignee_id == model.User.id
  2775. )
  2776. column = model.User.user
  2777. # If we are ordering by user, then order by reporters' usernames
  2778. elif order_key == "user":
  2779. # We must do a LEFT JOIN on model.Issue.user because there are
  2780. # two foreign keys on model.Issue tied to model.User. This tells
  2781. # SQLAlchemy which foreign key on model.User to order on.
  2782. query = query.outerjoin(
  2783. model.User, model.Issue.user_id == model.User.id
  2784. )
  2785. column = model.User.user
  2786. elif order_key in model.Issue.__table__.columns.keys():
  2787. column = getattr(model.Issue, order_key)
  2788. if ("%s" % column.type) == "TEXT":
  2789. column = func.lower(column)
  2790. # The priority is sorted differently because it is by weight and the lower
  2791. # the number, the higher the priority
  2792. if (order_key != "priority" and order == "asc") or (
  2793. order_key == "priority" and order == "desc"
  2794. ):
  2795. query = query.order_by(asc(column))
  2796. else:
  2797. query = query.order_by(desc(column))
  2798. if issueid is not None or issueuid is not None:
  2799. output = query.first()
  2800. elif count:
  2801. output = query.count()
  2802. else:
  2803. if offset is not None:
  2804. query = query.offset(offset)
  2805. if limit:
  2806. query = query.limit(limit)
  2807. output = query.all()
  2808. return output
  2809. def get_tags_of_project(session, project, pattern=None):
  2810. """Returns the list of tags associated with the issues of a project."""
  2811. query = (
  2812. session.query(model.TagColored)
  2813. .filter(model.TagColored.tag != "")
  2814. .filter(model.TagColored.project_id == project.id)
  2815. .order_by(model.TagColored.tag)
  2816. )
  2817. if pattern:
  2818. query = query.filter(
  2819. model.TagColored.tag.ilike(pattern.replace("*", "%"))
  2820. )
  2821. return query.all()
  2822. def get_tag(session, tag):
  2823. """Returns a Tag object for the given tag text."""
  2824. query = session.query(model.Tag).filter(model.Tag.tag == tag)
  2825. return query.first()
  2826. def get_colored_tag(session, tag, project_id):
  2827. """Returns a TagColored object for the given tag text."""
  2828. query = (
  2829. session.query(model.TagColored)
  2830. .filter(model.TagColored.tag == tag)
  2831. .filter(model.TagColored.project_id == project_id)
  2832. )
  2833. return query.first()
  2834. def search_pull_requests(
  2835. session,
  2836. requestid=None,
  2837. project_id=None,
  2838. project_id_from=None,
  2839. status=None,
  2840. author=None,
  2841. assignee=None,
  2842. tags=None,
  2843. count=False,
  2844. offset=None,
  2845. limit=None,
  2846. updated_after=None,
  2847. branch_from=None,
  2848. order="desc",
  2849. order_key=None,
  2850. search_pattern=None,
  2851. ):
  2852. """Retrieve the specified pull-requests."""
  2853. query = session.query(model.PullRequest)
  2854. # by default sort request by date_created.
  2855. column = model.PullRequest.date_created
  2856. if order_key == "last_updated":
  2857. # We actually want to order on updated_on and not last_updated
  2858. # https://pagure.io/pagure/issue/4464#comment-624915
  2859. column = model.PullRequest.updated_on
  2860. if requestid:
  2861. query = query.filter(model.PullRequest.id == requestid)
  2862. if updated_after:
  2863. query = query.filter(model.PullRequest.last_updated >= updated_after)
  2864. if project_id:
  2865. query = query.filter(model.PullRequest.project_id == project_id)
  2866. if project_id_from:
  2867. query = query.filter(
  2868. model.PullRequest.project_id_from == project_id_from
  2869. )
  2870. if status is not None:
  2871. if isinstance(status, bool):
  2872. if status:
  2873. query = query.filter(model.PullRequest.status == "Open")
  2874. else:
  2875. query = query.filter(model.PullRequest.status != "Open")
  2876. else:
  2877. query = query.filter(
  2878. func.lower(model.PullRequest.status) == status.lower()
  2879. )
  2880. if assignee is not None:
  2881. assignee = "%s" % assignee
  2882. if not pagure.utils.is_true(assignee, ["false", "0", "true", "1"]):
  2883. user2 = aliased(model.User)
  2884. if assignee.startswith("!"):
  2885. sub = (
  2886. session.query(model.PullRequest.uid)
  2887. .filter(model.PullRequest.assignee_id == user2.id)
  2888. .filter(user2.user == assignee[1:])
  2889. )
  2890. query = query.filter(~model.PullRequest.uid.in_(sub))
  2891. else:
  2892. query = query.filter(
  2893. model.PullRequest.assignee_id == user2.id
  2894. ).filter(user2.user == assignee)
  2895. elif pagure.utils.is_true(assignee):
  2896. query = query.filter(model.PullRequest.assignee_id.isnot(None))
  2897. else:
  2898. query = query.filter(model.PullRequest.assignee_id.is_(None))
  2899. if author is not None:
  2900. user3 = aliased(model.User)
  2901. query = query.filter(model.PullRequest.user_id == user3.id).filter(
  2902. user3.user == author
  2903. )
  2904. if branch_from is not None:
  2905. query = query.filter(model.PullRequest.branch_from == branch_from)
  2906. if tags is not None and tags != []:
  2907. if isinstance(tags, six.string_types):
  2908. tags = [tags]
  2909. notags = []
  2910. ytags = []
  2911. for tag in tags:
  2912. if tag.startswith("!"):
  2913. notags.append(tag[1:])
  2914. else:
  2915. ytags.append(tag)
  2916. if ytags:
  2917. sub_q2 = session.query(sqlalchemy.distinct(model.PullRequest.uid))
  2918. if project_id is not None:
  2919. sub_q2 = sub_q2.filter(
  2920. model.PullRequest.project_id == project_id
  2921. )
  2922. sub_q2 = (
  2923. sub_q2.filter(
  2924. model.PullRequest.uid == model.TagPullRequest.request_uid
  2925. )
  2926. .filter(model.TagPullRequest.tag_id == model.TagColored.id)
  2927. .filter(model.TagColored.tag.in_(ytags))
  2928. )
  2929. if notags:
  2930. sub_q3 = session.query(sqlalchemy.distinct(model.PullRequest.uid))
  2931. if project_id is not None:
  2932. sub_q3 = sub_q3.filter(
  2933. model.PullRequest.project_id == project_id
  2934. )
  2935. sub_q3 = (
  2936. sub_q3.filter(
  2937. model.PullRequest.uid == model.TagPullRequest.request_uid
  2938. )
  2939. .filter(model.TagPullRequest.tag_id == model.TagColored.id)
  2940. .filter(model.TagColored.tag.in_(notags))
  2941. )
  2942. # Adjust the main query based on the parameters specified
  2943. if ytags and not notags:
  2944. query = query.filter(model.PullRequest.uid.in_(sub_q2))
  2945. elif not ytags and notags:
  2946. query = query.filter(~model.PullRequest.uid.in_(sub_q3))
  2947. elif ytags and notags:
  2948. final_set = set([i[0] for i in sub_q2.all()]) - set(
  2949. [i[0] for i in sub_q3.all()]
  2950. )
  2951. if final_set:
  2952. query = query.filter(
  2953. model.PullRequest.uid.in_(list(final_set))
  2954. )
  2955. if search_pattern is not None:
  2956. if "*" in search_pattern:
  2957. search_pattern = search_pattern.replace("*", "%")
  2958. else:
  2959. search_pattern = "%%%s%%" % search_pattern
  2960. query = query.filter(model.PullRequest.title.ilike(search_pattern))
  2961. # Depending on the order, the query is sorted(default is desc)
  2962. if order == "asc":
  2963. query = query.order_by(asc(column))
  2964. else:
  2965. query = query.order_by(desc(column))
  2966. if requestid:
  2967. output = query.first()
  2968. elif count:
  2969. output = query.count()
  2970. else:
  2971. if offset:
  2972. query = query.offset(offset)
  2973. if limit:
  2974. query = query.limit(limit)
  2975. output = query.all()
  2976. return output
  2977. def reopen_pull_request(session, request, user):
  2978. """Re-Open the provided pull request"""
  2979. if request.status != "Closed":
  2980. raise pagure.exceptions.PagureException(
  2981. "Trying to reopen a pull request that is not closed"
  2982. )
  2983. user_obj = get_user(session, user)
  2984. request.status = "Open"
  2985. session.add(request)
  2986. session.flush()
  2987. log_action(session, request.status.lower(), request, user_obj)
  2988. pagure.lib.notify.notify_reopen_pull_request(request, user_obj)
  2989. pagure.lib.git.update_git(request, repo=request.project)
  2990. add_pull_request_comment(
  2991. session,
  2992. request,
  2993. commit=None,
  2994. tree_id=None,
  2995. filename=None,
  2996. row=None,
  2997. comment="Pull-Request has been reopened by %s" % (user),
  2998. user=user,
  2999. notify=False,
  3000. notification=True,
  3001. )
  3002. pagure.lib.notify.log(
  3003. request.project,
  3004. topic="pull-request.reopened",
  3005. msg=dict(
  3006. pullrequest=request.to_json(public=True), agent=user_obj.username
  3007. ),
  3008. )
  3009. def close_pull_request(session, request, user, merged=True):
  3010. """Close the provided pull-request."""
  3011. user_obj = get_user(session, user)
  3012. if merged is True:
  3013. request.status = "Merged"
  3014. else:
  3015. request.status = "Closed"
  3016. request.closed_by_id = user_obj.id
  3017. request.closed_at = datetime.datetime.utcnow()
  3018. session.add(request)
  3019. session.flush()
  3020. log_action(session, request.status.lower(), request, user_obj)
  3021. if merged is True:
  3022. pagure.lib.notify.notify_merge_pull_request(request, user_obj)
  3023. else:
  3024. pagure.lib.notify.notify_closed_pull_request(request, user_obj)
  3025. pagure.lib.git.update_git(request, repo=request.project)
  3026. add_pull_request_comment(
  3027. session,
  3028. request,
  3029. commit=None,
  3030. tree_id=None,
  3031. filename=None,
  3032. row=None,
  3033. comment="Pull-Request has been %s by %s"
  3034. % (request.status.lower(), user),
  3035. user=user,
  3036. notify=False,
  3037. notification=True,
  3038. )
  3039. pagure.lib.notify.log(
  3040. request.project,
  3041. topic="pull-request.closed",
  3042. msg=dict(
  3043. pullrequest=request.to_json(public=True),
  3044. merged=merged,
  3045. agent=user_obj.username,
  3046. ),
  3047. )
  3048. def reset_status_pull_request(session, project, but_uids=None):
  3049. """Reset the status of all opened Pull-Requests of a project."""
  3050. query = (
  3051. session.query(model.PullRequest)
  3052. .filter(model.PullRequest.project_id == project.id)
  3053. .filter(model.PullRequest.status == "Open")
  3054. )
  3055. if but_uids:
  3056. query = query.filter(model.PullRequest.uid.notin_(but_uids))
  3057. query.update(
  3058. {model.PullRequest.merge_status: None}, synchronize_session=False
  3059. )
  3060. session.commit()
  3061. def add_attachment(repo, issue, attachmentfolder, user, filename, filestream):
  3062. """Add a file to the attachments folder of repo and update git."""
  3063. _log.info(
  3064. "Adding file: %s to the git repo: %s",
  3065. repo.path,
  3066. werkzeug.utils.secure_filename(filename),
  3067. )
  3068. # Prefix the filename with a timestamp:
  3069. filename = "%s-%s" % (
  3070. hashlib.sha256(filestream.read()).hexdigest(),
  3071. werkzeug.utils.secure_filename(filename),
  3072. )
  3073. filedir = os.path.join(attachmentfolder, repo.fullname, "files")
  3074. filepath = os.path.join(filedir, filename)
  3075. if os.path.exists(filepath):
  3076. return filename
  3077. if not os.path.exists(filedir):
  3078. os.makedirs(filedir)
  3079. # Write file
  3080. filestream.seek(0)
  3081. with open(filepath, "wb") as stream:
  3082. stream.write(filestream.read())
  3083. pagure.lib.tasks.add_file_to_git.delay(
  3084. repo.name,
  3085. repo.namespace,
  3086. repo.user.username if repo.is_fork else None,
  3087. user.username,
  3088. issue.uid,
  3089. filename,
  3090. )
  3091. return filename
  3092. def get_issue_statuses(session):
  3093. """Return the complete list of status an issue can have."""
  3094. output = []
  3095. statuses = session.query(model.StatusIssue).all()
  3096. for status in statuses:
  3097. output.append(status.status)
  3098. return output
  3099. def get_issue_comment(session, issue_uid, comment_id):
  3100. """Return a specific comment of a specified issue."""
  3101. query = (
  3102. session.query(model.IssueComment)
  3103. .filter(model.IssueComment.issue_uid == issue_uid)
  3104. .filter(model.IssueComment.id == comment_id)
  3105. )
  3106. return query.first()
  3107. def get_issue_comment_by_user_and_comment(
  3108. session, issue_uid, user_id, content
  3109. ):
  3110. """Return a specific comment of a specified issue."""
  3111. query = (
  3112. session.query(model.IssueComment)
  3113. .filter(model.IssueComment.issue_uid == issue_uid)
  3114. .filter(model.IssueComment.user_id == user_id)
  3115. .filter(model.IssueComment.comment == content)
  3116. )
  3117. return query.first()
  3118. def get_request_comment(session, request_uid, comment_id):
  3119. """Return a specific comment of a specified request."""
  3120. query = (
  3121. session.query(model.PullRequestComment)
  3122. .filter(model.PullRequestComment.pull_request_uid == request_uid)
  3123. .filter(model.PullRequestComment.id == comment_id)
  3124. )
  3125. return query.first()
  3126. def get_issue_by_uid(session, issue_uid):
  3127. """Return the issue corresponding to the specified unique identifier.
  3128. :arg session: the session to use to connect to the database.
  3129. :arg issue_uid: the unique identifier of an issue. This identifier is
  3130. unique accross all projects on this pagure instance and should be
  3131. unique accross multiple pagure instances as well
  3132. :type issue_uid: str or None
  3133. :return: A single Issue object.
  3134. :rtype: pagure.lib.model.Issue
  3135. """
  3136. query = session.query(model.Issue).filter(model.Issue.uid == issue_uid)
  3137. return query.first()
  3138. def get_request_by_uid(session, request_uid):
  3139. """Return the request corresponding to the specified unique identifier.
  3140. :arg session: the session to use to connect to the database.
  3141. :arg request_uid: the unique identifier of a request. This identifier is
  3142. unique accross all projects on this pagure instance and should be
  3143. unique accross multiple pagure instances as well
  3144. :type request_uid: str or None
  3145. :return: A single Issue object.
  3146. :rtype: pagure.lib.model.PullRequest
  3147. """
  3148. query = session.query(model.PullRequest).filter(
  3149. model.PullRequest.uid == request_uid
  3150. )
  3151. return query.first()
  3152. def get_pull_request_flag_by_uid(session, request, flag_uid):
  3153. """Return the flag corresponding to the specified unique identifier.
  3154. :arg session: the session to use to connect to the database.
  3155. :arg request: the pull-request that was flagged
  3156. :arg flag_uid: the unique identifier of a request. This identifier is
  3157. unique accross all flags on this pagure instance and should be
  3158. unique accross multiple pagure instances as well
  3159. :type request_uid: str or None
  3160. :return: A single Issue object.
  3161. :rtype: pagure.lib.model.PullRequestFlag
  3162. """
  3163. query = (
  3164. session.query(model.PullRequestFlag)
  3165. .filter(model.PullRequestFlag.pull_request_uid == request.uid)
  3166. .filter(model.PullRequestFlag.uid == flag_uid.strip())
  3167. )
  3168. return query.first()
  3169. def get_commit_flag_by_uid(session, commit_hash, flag_uid):
  3170. """Return the flag corresponding to the specified unique identifier.
  3171. :arg session: the session to use to connect to the database.
  3172. :arg commit_hash: the hash of the commit that got flagged
  3173. :arg flag_uid: the unique identifier of a request. This identifier is
  3174. unique accross all flags on this pagure instance and should be
  3175. unique accross multiple pagure instances as well
  3176. :type request_uid: str or None
  3177. :return: A single Issue object.
  3178. :rtype: pagure.lib.model.PullRequestFlag
  3179. """
  3180. query = (
  3181. session.query(model.CommitFlag)
  3182. .filter(model.CommitFlag.commit_hash == commit_hash)
  3183. .filter(model.CommitFlag.uid == flag_uid.strip() if flag_uid else None)
  3184. )
  3185. return query.first()
  3186. def set_up_user(
  3187. session,
  3188. username,
  3189. fullname,
  3190. default_email,
  3191. emails=None,
  3192. ssh_key=None,
  3193. keydir=None,
  3194. ):
  3195. """Set up a new user into the database or update its information."""
  3196. user = search_user(session, username=username)
  3197. if not user:
  3198. user = model.User(
  3199. user=username, fullname=fullname, default_email=default_email
  3200. )
  3201. session.add(user)
  3202. session.flush()
  3203. if user.fullname != fullname:
  3204. user.fullname = fullname
  3205. session.add(user)
  3206. session.flush()
  3207. if emails:
  3208. emails = set(emails)
  3209. else:
  3210. emails = set()
  3211. emails.add(default_email)
  3212. for email in emails:
  3213. try:
  3214. add_email_to_user(session, user, email)
  3215. except pagure.exceptions.PagureException as err:
  3216. _log.exception(err)
  3217. if ssh_key and not user.sshkeys:
  3218. update_user_ssh(session, user, ssh_key, keydir)
  3219. return user
  3220. def allowed_emailaddress(email):
  3221. """check if email domains are restricted and if a given email address
  3222. is allowed."""
  3223. allowed_email_domains = pagure_config.get("ALLOWED_EMAIL_DOMAINS", None)
  3224. if allowed_email_domains:
  3225. for domain in allowed_email_domains:
  3226. if email.endswith(domain):
  3227. return
  3228. raise pagure.exceptions.PagureException(
  3229. "The email address "
  3230. + email
  3231. + " "
  3232. + "is not in the list of allowed email domains:\n"
  3233. + "\n".join(allowed_email_domains)
  3234. )
  3235. def add_email_to_user(session, user, user_email):
  3236. """Add the provided email to the specified user."""
  3237. try:
  3238. allowed_emailaddress(user_email)
  3239. except pagure.exceptions.PagureException:
  3240. raise
  3241. emails = [email.email for email in user.emails]
  3242. if user_email not in emails:
  3243. useremail = model.UserEmail(user_id=user.id, email=user_email)
  3244. session.add(useremail)
  3245. session.flush()
  3246. if email_logs_count(session, user_email):
  3247. update_log_email_user(session, user_email, user)
  3248. def update_user_ssh(session, user, ssh_key, keydir, update_only=False):
  3249. """Set up a new user into the database or update its information."""
  3250. if isinstance(user, six.string_types):
  3251. user = get_user(session, user)
  3252. if ssh_key:
  3253. for key in user.sshkeys:
  3254. session.delete(key)
  3255. for key in ssh_key.strip().split("\n"):
  3256. key = key.strip()
  3257. add_sshkey_to_project_or_user(
  3258. session=session,
  3259. ssh_key=key,
  3260. user=user,
  3261. pushaccess=True,
  3262. creator=user,
  3263. )
  3264. session.commit()
  3265. if keydir:
  3266. create_user_ssh_keys_on_disk(user, keydir)
  3267. if update_only:
  3268. pagure.lib.tasks.gitolite_post_compile_only.delay()
  3269. else:
  3270. pagure.lib.git.generate_gitolite_acls(project=None)
  3271. session.add(user)
  3272. session.flush()
  3273. def avatar_url_from_email(email, size=64, default="retro", dns=False):
  3274. """
  3275. Our own implementation since fas doesn't support this nicely yet.
  3276. """
  3277. if not email:
  3278. return
  3279. if dns: # pragma: no cover
  3280. # This makes an extra DNS SRV query, which can slow down our webapps.
  3281. # It is necessary for libravatar federation, though.
  3282. import libravatar
  3283. return libravatar.libravatar_url(
  3284. openid=email, size=size, default=default
  3285. )
  3286. else:
  3287. query = urlencode({"s": size, "d": default})
  3288. email = email.encode("utf-8")
  3289. hashhex = hashlib.sha256(email).hexdigest()
  3290. return "https://seccdn.libravatar.org/avatar/%s?%s" % (hashhex, query)
  3291. def update_tags(session, obj, tags, username):
  3292. """Update the tags of a specified object (adding or removing them).
  3293. This object can be either an issue or a project.
  3294. """
  3295. if isinstance(tags, six.string_types):
  3296. tags = [tags]
  3297. toadd = set(tags) - set(obj.tags_text)
  3298. torm = set(obj.tags_text) - set(tags)
  3299. messages = []
  3300. if toadd:
  3301. add_tag_obj(session, obj=obj, tags=toadd, user=username)
  3302. messages.append(
  3303. "%s tagged with: %s"
  3304. % (obj.isa.capitalize(), ", ".join(sorted(toadd)))
  3305. )
  3306. if torm:
  3307. remove_tags_obj(session, obj=obj, tags=torm, user=username)
  3308. messages.append(
  3309. "%s **un**tagged with: %s"
  3310. % (obj.isa.capitalize(), ", ".join(sorted(torm)))
  3311. )
  3312. session.commit()
  3313. return messages
  3314. def update_dependency_issue(session, repo, issue, depends, username):
  3315. """Update the dependency of a specified issue (adding or removing them)"""
  3316. if isinstance(depends, six.string_types):
  3317. depends = [depends]
  3318. toadd = set(depends) - set(issue.depending_text)
  3319. torm = set(issue.depending_text) - set(depends)
  3320. messages = []
  3321. # Add issue depending
  3322. for depend in sorted([int(i) for i in toadd]):
  3323. messages.append("Issue marked as depending on: #%s" % depend)
  3324. issue_depend = search_issues(session, repo, issueid=depend)
  3325. if issue_depend is None:
  3326. continue
  3327. if issue_depend.id in issue.depending_text: # pragma: no cover
  3328. # we should never be in this case but better safe than sorry...
  3329. continue
  3330. add_issue_dependency(
  3331. session, issue=issue_depend, issue_blocked=issue, user=username
  3332. )
  3333. # Remove issue depending
  3334. for depend in sorted([int(i) for i in torm]):
  3335. messages.append("Issue **un**marked as depending on: #%s" % depend)
  3336. issue_depend = search_issues(session, repo, issueid=depend)
  3337. if issue_depend is None: # pragma: no cover
  3338. # We cannot test this as it would mean we managed to put in an
  3339. # invalid ticket as dependency earlier
  3340. continue
  3341. if issue_depend.id not in issue.depending_text: # pragma: no cover
  3342. # we should never be in this case but better safe than sorry...
  3343. continue
  3344. remove_issue_dependency(
  3345. session, issue=issue, issue_blocked=issue_depend, user=username
  3346. )
  3347. session.commit()
  3348. return messages
  3349. def update_blocked_issue(session, repo, issue, blocks, username):
  3350. """Update the upstream dependency of a specified issue (adding or
  3351. removing them)
  3352. """
  3353. if isinstance(blocks, six.string_types):
  3354. blocks = [blocks]
  3355. toadd = set(blocks) - set(issue.blocking_text)
  3356. torm = set(issue.blocking_text) - set(blocks)
  3357. messages = []
  3358. # Add issue blocked
  3359. for block in sorted([int(i) for i in toadd]):
  3360. messages.append("Issue marked as blocking: #%s" % block)
  3361. issue_block = search_issues(session, repo, issueid=block)
  3362. if issue_block is None:
  3363. continue
  3364. if issue_block.id in issue.blocking_text: # pragma: no cover
  3365. # we should never be in this case but better safe than sorry...
  3366. continue
  3367. add_issue_dependency(
  3368. session, issue=issue, issue_blocked=issue_block, user=username
  3369. )
  3370. session.commit()
  3371. # Remove issue blocked
  3372. for block in sorted([int(i) for i in torm]):
  3373. messages.append("Issue **un**marked as blocking: #%s" % block)
  3374. issue_block = search_issues(session, repo, issueid=block)
  3375. if issue_block is None: # pragma: no cover
  3376. # We cannot test this as it would mean we managed to put in an
  3377. # invalid ticket as dependency earlier
  3378. continue
  3379. if issue_block.id not in issue.blocking_text: # pragma: no cover
  3380. # we should never be in this case but better safe than sorry...
  3381. continue
  3382. remove_issue_dependency(
  3383. session, issue=issue_block, issue_blocked=issue, user=username
  3384. )
  3385. session.commit()
  3386. return messages
  3387. def add_user_pending_email(session, userobj, email):
  3388. """Add the provided email to the specified user."""
  3389. try:
  3390. allowed_emailaddress(email)
  3391. except pagure.exceptions.PagureException:
  3392. raise
  3393. other_user = search_user(session, email=email)
  3394. if other_user and other_user != userobj:
  3395. raise pagure.exceptions.PagureException(
  3396. "Someone else has already registered this email"
  3397. )
  3398. pending_email = search_pending_email(session, email=email)
  3399. if pending_email:
  3400. raise pagure.exceptions.PagureException(
  3401. "This email is already pending confirmation"
  3402. )
  3403. tmpemail = pagure.lib.model.UserEmailPending(
  3404. user_id=userobj.id,
  3405. token=pagure.lib.login.id_generator(40),
  3406. email=email,
  3407. )
  3408. session.add(tmpemail)
  3409. session.flush()
  3410. pagure.lib.notify.notify_new_email(tmpemail, user=userobj)
  3411. def resend_pending_email(session, userobj, email):
  3412. """Resend to the user the confirmation email for the provided email
  3413. address.
  3414. """
  3415. other_user = search_user(session, email=email)
  3416. if other_user and other_user != userobj:
  3417. raise pagure.exceptions.PagureException(
  3418. "Someone else has already registered this email address"
  3419. )
  3420. pending_email = search_pending_email(session, email=email)
  3421. if not pending_email:
  3422. raise pagure.exceptions.PagureException(
  3423. "This email address has already been confirmed"
  3424. )
  3425. pending_email.token = pagure.lib.login.id_generator(40)
  3426. session.add(pending_email)
  3427. session.flush()
  3428. pagure.lib.notify.notify_new_email(pending_email, user=userobj)
  3429. def search_pending_email(session, email=None, token=None):
  3430. """Searches the database for the pending email matching the given
  3431. criterias.
  3432. :arg session: the session to use to connect to the database.
  3433. :kwarg email: the email to look for
  3434. :type email: string or None
  3435. :kwarg token: the token of the pending email to look for
  3436. :type token: string or None
  3437. :return: A single UserEmailPending object
  3438. :rtype: UserEmailPending
  3439. """
  3440. query = session.query(model.UserEmailPending)
  3441. if email is not None:
  3442. query = query.filter(model.UserEmailPending.email == email)
  3443. if token is not None:
  3444. query = query.filter(model.UserEmailPending.token == token)
  3445. output = query.first()
  3446. return output
  3447. def generate_hook_token(session):
  3448. """For each project in the database, re-generate a unique hook_token."""
  3449. for project in search_projects(session):
  3450. project.hook_token = pagure.lib.login.id_generator(40)
  3451. session.add(project)
  3452. session.commit()
  3453. def get_group_types(session, group_type=None):
  3454. """Return the list of type a group can have."""
  3455. query = session.query(model.PagureGroupType).order_by(
  3456. model.PagureGroupType.group_type
  3457. )
  3458. if group_type:
  3459. query = query.filter(model.PagureGroupType.group_type == group_type)
  3460. return query.all()
  3461. def search_groups(
  3462. session,
  3463. pattern=None,
  3464. group_name=None,
  3465. group_type=None,
  3466. display_name=None,
  3467. offset=None,
  3468. limit=None,
  3469. count=False,
  3470. ):
  3471. """Return the groups based on the criteria specified."""
  3472. query = session.query(model.PagureGroup).order_by(
  3473. model.PagureGroup.group_type, model.PagureGroup.group_name
  3474. )
  3475. if pattern:
  3476. pattern = pattern.replace("*", "%")
  3477. query = query.filter(
  3478. sqlalchemy.or_(
  3479. model.PagureGroup.group_name.ilike(pattern),
  3480. model.PagureGroup.display_name.ilike(pattern),
  3481. )
  3482. )
  3483. if group_name:
  3484. query = query.filter(model.PagureGroup.group_name == group_name)
  3485. if display_name:
  3486. query = query.filter(model.PagureGroup.display_name == display_name)
  3487. if group_type:
  3488. query = query.filter(model.PagureGroup.group_type == group_type)
  3489. if offset:
  3490. query = query.offset(offset)
  3491. if limit:
  3492. query = query.limit(limit)
  3493. if group_name:
  3494. return query.first()
  3495. elif count:
  3496. return query.count()
  3497. else:
  3498. return query.all()
  3499. def add_user_to_group(
  3500. session, username, group, user, is_admin, from_external=False
  3501. ):
  3502. """Add the specified user to the given group.
  3503. from_external indicates whether this is a remotely synced group.
  3504. """
  3505. new_user = search_user(session, username=username)
  3506. if not new_user:
  3507. raise pagure.exceptions.PagureException(
  3508. "No user `%s` found" % username
  3509. )
  3510. action_user = user
  3511. user = search_user(session, username=user)
  3512. if not user:
  3513. raise pagure.exceptions.PagureException(
  3514. "No user `%s` found" % action_user
  3515. )
  3516. if (
  3517. not from_external
  3518. and group.group_name not in user.groups
  3519. and not is_admin
  3520. and user.username != group.creator.username
  3521. ):
  3522. raise pagure.exceptions.PagureException(
  3523. "You are not allowed to add user to this group"
  3524. )
  3525. for guser in group.users:
  3526. if guser.username == new_user.username:
  3527. return "User `%s` already in the group, nothing to change." % (
  3528. new_user.username
  3529. )
  3530. grp = model.PagureUserGroup(group_id=group.id, user_id=new_user.id)
  3531. session.add(grp)
  3532. session.flush()
  3533. return "User `%s` added to the group `%s`." % (
  3534. new_user.username,
  3535. group.group_name,
  3536. )
  3537. def edit_group_info(session, group, display_name, description, user, is_admin):
  3538. """Edit the information regarding a given group."""
  3539. action_user = user
  3540. user = search_user(session, username=user)
  3541. if not user:
  3542. raise pagure.exceptions.PagureException(
  3543. "No user `%s` found" % action_user
  3544. )
  3545. if (
  3546. group.group_name not in user.groups
  3547. and not is_admin
  3548. and user.username != group.creator.username
  3549. ):
  3550. raise pagure.exceptions.PagureException(
  3551. "You are not allowed to edit this group"
  3552. )
  3553. edits = []
  3554. if display_name and display_name != group.display_name:
  3555. group.display_name = display_name
  3556. edits.append("display_name")
  3557. if description and description != group.description:
  3558. group.description = description
  3559. edits.append("description")
  3560. session.add(group)
  3561. session.flush()
  3562. msg = "Nothing changed"
  3563. if edits:
  3564. pagure.lib.notify.log(
  3565. None,
  3566. topic="group.edit",
  3567. msg=dict(
  3568. group=group.to_json(public=True),
  3569. fields=edits,
  3570. agent=user.username,
  3571. ),
  3572. )
  3573. msg = 'Group "%s" (%s) edited' % (group.display_name, group.group_name)
  3574. return msg
  3575. def delete_user_of_group(
  3576. session,
  3577. username,
  3578. groupname,
  3579. user,
  3580. is_admin,
  3581. force=False,
  3582. from_external=False,
  3583. ):
  3584. """Removes the specified user from the given group."""
  3585. group_obj = search_groups(session, group_name=groupname)
  3586. if not group_obj:
  3587. raise pagure.exceptions.PagureException(
  3588. "No group `%s` found" % groupname
  3589. )
  3590. drop_user = search_user(session, username=username)
  3591. if not drop_user:
  3592. raise pagure.exceptions.PagureException(
  3593. "No user `%s` found" % username
  3594. )
  3595. action_user = user
  3596. user = search_user(session, username=user)
  3597. if not user:
  3598. raise pagure.exceptions.PagureException(
  3599. "Could not find user %s" % action_user
  3600. )
  3601. if (
  3602. not from_external
  3603. and group_obj.group_name not in user.groups
  3604. and not is_admin
  3605. ):
  3606. raise pagure.exceptions.PagureException(
  3607. "You are not allowed to remove user from this group"
  3608. )
  3609. if drop_user.username == group_obj.creator.username and not force:
  3610. raise pagure.exceptions.PagureException(
  3611. "The creator of a group cannot be removed"
  3612. )
  3613. user_grp = get_user_group(session, drop_user.id, group_obj.id)
  3614. if not user_grp:
  3615. raise pagure.exceptions.PagureException(
  3616. "User `%s` could not be found in the group `%s`"
  3617. % (username, groupname)
  3618. )
  3619. session.delete(user_grp)
  3620. session.flush()
  3621. def add_group(
  3622. session,
  3623. group_name,
  3624. display_name,
  3625. description,
  3626. group_type,
  3627. user,
  3628. is_admin,
  3629. blacklist,
  3630. ):
  3631. """Creates a new group with the given information."""
  3632. if " " in group_name:
  3633. raise pagure.exceptions.PagureException(
  3634. "Spaces are not allowed in group names: %s" % group_name
  3635. )
  3636. if group_name in blacklist:
  3637. raise pagure.exceptions.PagureException(
  3638. "This group name has been blacklisted, "
  3639. "please choose another one"
  3640. )
  3641. group_types = ["user"]
  3642. if is_admin:
  3643. group_types = [grp.group_type for grp in get_group_types(session)]
  3644. if not is_admin:
  3645. group_type = "user"
  3646. if group_type not in group_types:
  3647. raise pagure.exceptions.PagureException("Invalide type for this group")
  3648. username = user
  3649. user = search_user(session, username=user)
  3650. if not user:
  3651. raise pagure.exceptions.PagureException(
  3652. "Could not find user %s" % username
  3653. )
  3654. group = search_groups(session, group_name=group_name)
  3655. if group:
  3656. raise pagure.exceptions.PagureException(
  3657. "There is already a group named %s" % group_name
  3658. )
  3659. display = search_groups(session, display_name=display_name)
  3660. if display:
  3661. raise pagure.exceptions.PagureException(
  3662. "There is already a group with display name `%s` created."
  3663. % display_name
  3664. )
  3665. grp = pagure.lib.model.PagureGroup(
  3666. group_name=group_name,
  3667. display_name=display_name,
  3668. description=description,
  3669. group_type=group_type,
  3670. user_id=user.id,
  3671. )
  3672. session.add(grp)
  3673. session.flush()
  3674. return add_user_to_group(
  3675. session, user.username, grp, user.username, is_admin
  3676. )
  3677. def get_user_group(session, userid, groupid):
  3678. """Return a specific user_group for the specified group and user
  3679. identifiers.
  3680. :arg session: the session with which to connect to the database.
  3681. """
  3682. query = (
  3683. session.query(model.PagureUserGroup)
  3684. .filter(model.PagureUserGroup.user_id == userid)
  3685. .filter(model.PagureUserGroup.group_id == groupid)
  3686. )
  3687. return query.first()
  3688. def is_group_member(session, user, groupname):
  3689. """Return whether the user is a member of the specified group."""
  3690. if not user:
  3691. return False
  3692. user = search_user(session, username=user)
  3693. if not user:
  3694. return False
  3695. return groupname in user.groups
  3696. def get_api_token(session, token_str):
  3697. """Return the Token object corresponding to the provided token string
  3698. if there is any, returns None otherwise.
  3699. """
  3700. query = session.query(model.Token).filter(model.Token.id == token_str)
  3701. return query.first()
  3702. def get_acls(session, restrict=None):
  3703. """Returns all the possible ACLs a token can have according to the
  3704. database.
  3705. """
  3706. query = session.query(model.ACL).order_by(model.ACL.name)
  3707. if restrict:
  3708. if isinstance(restrict, list):
  3709. query = query.filter(model.ACL.name.in_(restrict))
  3710. else:
  3711. query = query.filter(model.ACL.name == restrict)
  3712. return query.all()
  3713. def add_token_to_user(
  3714. session, project, acls, username, expiration_date, description=None
  3715. ):
  3716. """Create a new token for the specified user on the specified project
  3717. with the given ACLs.
  3718. """
  3719. acls_obj = session.query(model.ACL).filter(model.ACL.name.in_(acls)).all()
  3720. user = search_user(session, username=username)
  3721. if expiration_date > (
  3722. datetime.date.today() + datetime.timedelta(days=730)
  3723. ):
  3724. raise pagure.exceptions.PagureException(
  3725. "API tokens can only be created up to 2 years"
  3726. )
  3727. token = pagure.lib.model.Token(
  3728. id=pagure.lib.login.id_generator(64),
  3729. user_id=user.id,
  3730. project_id=project.id if project else None,
  3731. description=description,
  3732. expiration=expiration_date,
  3733. )
  3734. session.add(token)
  3735. session.flush()
  3736. for acl in acls_obj:
  3737. item = pagure.lib.model.TokenAcl(token_id=token.id, acl_id=acl.id)
  3738. session.add(item)
  3739. session.commit()
  3740. return token
  3741. def _convert_markdown(md_processor, text):
  3742. """Small function converting the text to html using the given markdown
  3743. processor.
  3744. This was done in order to make testing it easier.
  3745. """
  3746. return md_processor.convert(text)
  3747. def text2markdown(text, extended=True, readme=False):
  3748. """Simple text to html converter using the markdown library."""
  3749. extensions = [
  3750. "markdown.extensions.def_list",
  3751. "markdown.extensions.fenced_code",
  3752. "markdown.extensions.tables",
  3753. # All of the above are the .extra extensions
  3754. # w/o the "attribute lists" one
  3755. "markdown.extensions.admonition",
  3756. "markdown.extensions.codehilite",
  3757. "markdown.extensions.sane_lists",
  3758. "markdown.extensions.toc",
  3759. ]
  3760. # smart_strong is not an extension anymore in markdown 3.0+
  3761. try:
  3762. md_version = markdown.__version__.version_info
  3763. except AttributeError: # pragma: no cover
  3764. md_version = markdown.__version_info__
  3765. if md_version < (3, 0, 0):
  3766. extensions.append("markdown.extensions.smart_strong")
  3767. # Some extensions are enabled for READMEs and disabled otherwise
  3768. if readme:
  3769. extensions.extend(
  3770. ["markdown.extensions.abbr", "markdown.extensions.footnotes"]
  3771. )
  3772. else:
  3773. extensions.append("markdown.extensions.nl2br")
  3774. if extended:
  3775. # Install our markdown modifications
  3776. extensions.append("pagure.pfmarkdown")
  3777. md_processor = markdown.Markdown(
  3778. extensions=extensions,
  3779. extension_configs={
  3780. "markdown.extensions.codehilite": {"guess_lang": False}
  3781. },
  3782. output_format="xhtml5",
  3783. )
  3784. if text:
  3785. try:
  3786. text = _convert_markdown(md_processor, text)
  3787. except Exception as err:
  3788. print(err)
  3789. _log.debug(
  3790. "A markdown error occured while processing: ``%s``", text
  3791. )
  3792. return clean_input(text)
  3793. return ""
  3794. def filter_img_src(name, value):
  3795. """Filter in img html tags images coming from a different domain."""
  3796. if name in ("alt", "height", "width", "class", "data-src"):
  3797. return True
  3798. if name == "src":
  3799. parsed = urlparse(value)
  3800. return (not parsed.netloc) or parsed.netloc == urlparse(
  3801. pagure_config["APP_URL"]
  3802. ).netloc
  3803. return False
  3804. def clean_input(text, ignore=None):
  3805. """For a given html text, escape everything we do not want to support
  3806. to avoid potential security breach.
  3807. """
  3808. if ignore and not isinstance(ignore, (tuple, set, list)):
  3809. ignore = [ignore]
  3810. bleach_v = bleach.__version__.split(".")
  3811. for idx, val in enumerate(bleach_v):
  3812. try:
  3813. val = int(val)
  3814. except ValueError: # pragma: no cover
  3815. pass
  3816. bleach_v[idx] = val
  3817. attrs = bleach.ALLOWED_ATTRIBUTES.copy()
  3818. attrs["table"] = ["class"]
  3819. attrs["span"] = ["class", "id"]
  3820. attrs["div"] = ["class", "id"]
  3821. attrs["td"] = ["align", "class"]
  3822. attrs["th"] = ["align"]
  3823. attrs["a"].extend(["id", "data-line-number"])
  3824. if not ignore or "img" not in ignore:
  3825. # newer bleach need three args for attribute callable
  3826. if tuple(bleach_v) >= (2, 0, 0): # pragma: no cover
  3827. attrs["img"] = lambda tag, name, val: filter_img_src(name, val)
  3828. else:
  3829. attrs["img"] = filter_img_src
  3830. tags = bleach.ALLOWED_TAGS + [
  3831. "p",
  3832. "br",
  3833. "div",
  3834. "h1",
  3835. "h2",
  3836. "h3",
  3837. "h4",
  3838. "h5",
  3839. "h6",
  3840. "table",
  3841. "td",
  3842. "tr",
  3843. "th",
  3844. "thead",
  3845. "tbody",
  3846. "col",
  3847. "pre",
  3848. "img",
  3849. "hr",
  3850. "dl",
  3851. "dt",
  3852. "dd",
  3853. "span",
  3854. "kbd",
  3855. "var",
  3856. "del",
  3857. "cite",
  3858. "noscript",
  3859. "colgroup",
  3860. ]
  3861. if ignore:
  3862. for tag in ignore:
  3863. if tag in tags:
  3864. tags.remove(tag)
  3865. kwargs = {"tags": tags, "attributes": attrs}
  3866. # newer bleach allow to customize the protocol supported
  3867. if tuple(bleach_v) >= (1, 5, 0): # pragma: no cover
  3868. protocols = bleach.ALLOWED_PROTOCOLS + ["irc", "ircs"]
  3869. kwargs["protocols"] = protocols
  3870. return bleach.clean(text, **kwargs)
  3871. def could_be_text(text):
  3872. """Returns whether we think this chain of character could be text or not"""
  3873. try:
  3874. text.decode("utf-8")
  3875. return True
  3876. except (UnicodeDecodeError, UnicodeEncodeError):
  3877. return False
  3878. def get_pull_request_of_user(
  3879. session,
  3880. username,
  3881. status=None,
  3882. filed=None,
  3883. actionable=None,
  3884. offset=None,
  3885. limit=None,
  3886. created_since=None,
  3887. created_until=None,
  3888. updated_since=None,
  3889. updated_until=None,
  3890. closed_since=None,
  3891. closed_until=None,
  3892. count=False,
  3893. ):
  3894. """List the opened pull-requests of an user.
  3895. These pull-requests have either been opened by that user or against
  3896. projects that user has commit on.
  3897. If filed: only the PRs opened/filed by the specified username will be
  3898. returned.
  3899. If actionable: only the PRs not opened/filed by the specified username
  3900. will be returned.
  3901. """
  3902. projects = session.query(sqlalchemy.distinct(model.Project.id))
  3903. projects = projects.filter(
  3904. # User created the project
  3905. sqlalchemy.and_(
  3906. model.User.user == username, model.User.id == model.Project.user_id
  3907. )
  3908. )
  3909. sub_q2 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  3910. # User got commit right
  3911. sqlalchemy.and_(
  3912. model.User.user == username,
  3913. model.User.id == model.ProjectUser.user_id,
  3914. model.ProjectUser.project_id == model.Project.id,
  3915. sqlalchemy.or_(
  3916. model.ProjectUser.access == "admin",
  3917. model.ProjectUser.access == "commit",
  3918. ),
  3919. )
  3920. )
  3921. sub_q3 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  3922. # User created a group that has commit right
  3923. sqlalchemy.and_(
  3924. model.User.user == username,
  3925. model.PagureGroup.user_id == model.User.id,
  3926. model.PagureGroup.group_type == "user",
  3927. model.PagureGroup.id == model.ProjectGroup.group_id,
  3928. model.Project.id == model.ProjectGroup.project_id,
  3929. sqlalchemy.or_(
  3930. model.ProjectGroup.access == "admin",
  3931. model.ProjectGroup.access == "commit",
  3932. ),
  3933. )
  3934. )
  3935. sub_q4 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  3936. # User is part of a group that has commit right
  3937. sqlalchemy.and_(
  3938. model.User.user == username,
  3939. model.PagureUserGroup.user_id == model.User.id,
  3940. model.PagureUserGroup.group_id == model.PagureGroup.id,
  3941. model.PagureGroup.group_type == "user",
  3942. model.PagureGroup.id == model.ProjectGroup.group_id,
  3943. model.Project.id == model.ProjectGroup.project_id,
  3944. sqlalchemy.or_(
  3945. model.ProjectGroup.access == "admin",
  3946. model.ProjectGroup.access == "commit",
  3947. ),
  3948. )
  3949. )
  3950. projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
  3951. query = session.query(sqlalchemy.distinct(model.PullRequest.uid)).filter(
  3952. model.PullRequest.project_id.in_(projects.subquery())
  3953. )
  3954. query_2 = session.query(sqlalchemy.distinct(model.PullRequest.uid)).filter(
  3955. # User open the PR
  3956. sqlalchemy.and_(
  3957. model.PullRequest.user_id == model.User.id,
  3958. model.User.user == username,
  3959. )
  3960. )
  3961. final_sub = query.union(query_2)
  3962. query = (
  3963. session.query(model.PullRequest)
  3964. .filter(model.PullRequest.uid.in_(final_sub.subquery()))
  3965. .order_by(model.PullRequest.date_created.desc())
  3966. )
  3967. if status:
  3968. query = query.filter(model.PullRequest.status == status)
  3969. if filed:
  3970. query = query.filter(
  3971. model.PullRequest.user_id == model.User.id,
  3972. model.User.user == filed,
  3973. )
  3974. elif actionable:
  3975. query = query.filter(
  3976. model.PullRequest.user_id == model.User.id,
  3977. model.User.user != actionable,
  3978. )
  3979. if created_since:
  3980. query = query.filter(model.PullRequest.date_created >= created_since)
  3981. if created_until:
  3982. query = query.filter(model.PullRequest.date_created <= created_until)
  3983. if updated_since:
  3984. query = query.filter(model.PullRequest.updated_on <= updated_since)
  3985. if updated_until:
  3986. query = query.filter(model.PullRequest.updated_on <= updated_until)
  3987. if closed_since:
  3988. query = query.filter(model.PullRequest.closed_at <= closed_since)
  3989. if closed_until:
  3990. query = query.filter(model.PullRequest.closed_at <= closed_until)
  3991. if offset:
  3992. query = query.offset(offset)
  3993. if limit:
  3994. query = query.limit(limit)
  3995. if count:
  3996. return query.count()
  3997. else:
  3998. return query.all()
  3999. def update_watch_status(session, project, user, watch):
  4000. """Update the user status for watching a project.
  4001. The watch status can be:
  4002. -1: reset the watch status to default
  4003. 0: unwatch, don't notify the user of anything
  4004. 1: watch issues and PRs
  4005. 2: watch commits
  4006. 3: watch issues, PRs and commits
  4007. """
  4008. if watch not in ["-1", "0", "1", "2", "3"]:
  4009. raise pagure.exceptions.PagureException(
  4010. 'The watch value of "%s" is invalid' % watch
  4011. )
  4012. user_obj = get_user(session, user)
  4013. watcher = (
  4014. session.query(model.Watcher)
  4015. .filter(
  4016. sqlalchemy.and_(
  4017. model.Watcher.project_id == project.id,
  4018. model.Watcher.user_id == user_obj.id,
  4019. )
  4020. )
  4021. .first()
  4022. )
  4023. if watch == "-1":
  4024. if not watcher:
  4025. return "Watch status is already reset"
  4026. session.delete(watcher)
  4027. session.flush()
  4028. return "Watch status reset"
  4029. should_watch_issues = False
  4030. should_watch_commits = False
  4031. if watch == "1":
  4032. should_watch_issues = True
  4033. elif watch == "2":
  4034. should_watch_commits = True
  4035. elif watch == "3":
  4036. should_watch_issues = True
  4037. should_watch_commits = True
  4038. if not watcher:
  4039. watcher = model.Watcher(
  4040. project_id=project.id,
  4041. user_id=user_obj.id,
  4042. watch_issues=should_watch_issues,
  4043. watch_commits=should_watch_commits,
  4044. )
  4045. else:
  4046. watcher.watch_issues = should_watch_issues
  4047. watcher.watch_commits = should_watch_commits
  4048. session.add(watcher)
  4049. session.flush()
  4050. if should_watch_issues and should_watch_commits:
  4051. return "You are now watching issues, PRs, and commits on this project"
  4052. elif should_watch_issues:
  4053. return "You are now watching issues and PRs on this project"
  4054. elif should_watch_commits:
  4055. return "You are now watching commits on this project"
  4056. else:
  4057. return "You are no longer watching this project"
  4058. def get_watch_level_on_repo(
  4059. session, user, repo, repouser=None, namespace=None
  4060. ):
  4061. """Get a list representing the watch level of the user on the project."""
  4062. # If a user wasn't passed in, we can't determine their watch level
  4063. if user is None:
  4064. return []
  4065. elif isinstance(user, six.string_types):
  4066. user_obj = search_user(session, username=user)
  4067. else:
  4068. user_obj = search_user(session, username=user.username)
  4069. # If we can't find the user in the database, we can't determine their
  4070. # watch level
  4071. if not user_obj:
  4072. return []
  4073. # If the project passed in a Project for the repo parameter, then we
  4074. # don't need to query for it
  4075. if isinstance(repo, model.Project):
  4076. project = repo
  4077. # If the project passed in a string, then assume it is a project name
  4078. elif isinstance(repo, six.string_types):
  4079. project = _get_project(
  4080. session, repo, user=repouser, namespace=namespace
  4081. )
  4082. else:
  4083. raise RuntimeError(
  4084. 'The passed in repo is an invalid type of "{0}"'.format(
  4085. type(repo).__name__
  4086. )
  4087. )
  4088. # If the project is not found, we can't determine the involvement of the
  4089. # user in the project
  4090. if not project:
  4091. return []
  4092. query = (
  4093. session.query(model.Watcher)
  4094. .filter(model.Watcher.user_id == user_obj.id)
  4095. .filter(model.Watcher.project_id == project.id)
  4096. )
  4097. watcher = query.first()
  4098. # If there is a watcher issue, that means the user explicitly set a watch
  4099. # level on the project
  4100. if watcher:
  4101. if watcher.watch_issues and watcher.watch_commits:
  4102. return ["issues", "commits"]
  4103. elif watcher.watch_issues:
  4104. return ["issues"]
  4105. elif watcher.watch_commits:
  4106. return ["commits"]
  4107. else:
  4108. # If a watcher entry is set and both are set to False, that
  4109. # means the user explicitly asked to not be notified
  4110. return []
  4111. # If the user is the project owner, by default they will be watching
  4112. # issues and PRs
  4113. if user_obj.username == project.user.username:
  4114. return ["issues"]
  4115. # If the user is a contributor, by default they will be watching issues
  4116. # and PRs
  4117. for contributor in project.users:
  4118. if user_obj.username == contributor.username:
  4119. return ["issues"]
  4120. # If the user is in a project group, by default they will be watching
  4121. # issues and PRs
  4122. for group in project.groups:
  4123. for guser in group.users:
  4124. if user_obj.username == guser.username:
  4125. return ["issues"]
  4126. # If no other condition is true, then they are not explicitly watching
  4127. # the project or are not involved in the project to the point that
  4128. # comes with aq default watch level
  4129. return []
  4130. def user_watch_list(session, user, exclude_groups=None):
  4131. """Returns list of all the projects which the user is watching"""
  4132. user_obj = search_user(session, username=user)
  4133. if not user_obj:
  4134. return []
  4135. unwatched = (
  4136. session.query(model.Watcher)
  4137. .filter(model.Watcher.user_id == user_obj.id)
  4138. .filter(model.Watcher.watch_issues == False) # noqa: E712
  4139. .filter(model.Watcher.watch_commits == False) # noqa: E712
  4140. )
  4141. unwatched_list = []
  4142. if unwatched:
  4143. unwatched_list = [unwatch.project for unwatch in unwatched.all()]
  4144. watched = (
  4145. session.query(model.Watcher)
  4146. .filter(model.Watcher.user_id == user_obj.id)
  4147. .filter(model.Watcher.watch_issues == True) # noqa: E712
  4148. .filter(model.Watcher.watch_commits == True) # noqa: E712
  4149. )
  4150. watched_list = []
  4151. if watched:
  4152. watched_list = [watch.project for watch in watched.all()]
  4153. user_projects = search_projects(
  4154. session, username=user_obj.user, exclude_groups=exclude_groups
  4155. )
  4156. watch = set(watched_list + user_projects)
  4157. for project in user_projects:
  4158. if project in unwatched_list:
  4159. watch.remove(project)
  4160. return sorted(list(watch), key=lambda proj: proj.name)
  4161. def set_watch_obj(session, user, obj, watch_status):
  4162. """Set the watch status of the user on the specified object.
  4163. Objects can be either an issue or a pull-request
  4164. """
  4165. user_obj = get_user(session, user)
  4166. if obj.isa == "issue":
  4167. query = (
  4168. session.query(model.IssueWatcher)
  4169. .filter(model.IssueWatcher.user_id == user_obj.id)
  4170. .filter(model.IssueWatcher.issue_uid == obj.uid)
  4171. )
  4172. elif obj.isa == "pull-request":
  4173. query = (
  4174. session.query(model.PullRequestWatcher)
  4175. .filter(model.PullRequestWatcher.user_id == user_obj.id)
  4176. .filter(model.PullRequestWatcher.pull_request_uid == obj.uid)
  4177. )
  4178. else:
  4179. raise pagure.exceptions.InvalidObjectException(
  4180. 'Unsupported object found: "%s"' % obj
  4181. )
  4182. dbobj = query.first()
  4183. if not dbobj:
  4184. if obj.isa == "issue":
  4185. dbobj = model.IssueWatcher(
  4186. user_id=user_obj.id, issue_uid=obj.uid, watch=watch_status
  4187. )
  4188. elif obj.isa == "pull-request":
  4189. dbobj = model.PullRequestWatcher(
  4190. user_id=user_obj.id,
  4191. pull_request_uid=obj.uid,
  4192. watch=watch_status,
  4193. )
  4194. else:
  4195. dbobj.watch = watch_status
  4196. session.add(dbobj)
  4197. output = "You are no longer watching this %s" % obj.isa
  4198. if watch_status:
  4199. output = "You are now watching this %s" % obj.isa
  4200. return output
  4201. def get_watch_list(session, obj):
  4202. """Return a list of all the users that are watching the "object" """
  4203. private = False
  4204. if obj.isa == "issue":
  4205. private = obj.private
  4206. obj_watchers_query = session.query(model.IssueWatcher).filter(
  4207. model.IssueWatcher.issue_uid == obj.uid
  4208. )
  4209. elif obj.isa == "pull-request":
  4210. obj_watchers_query = session.query(model.PullRequestWatcher).filter(
  4211. model.PullRequestWatcher.pull_request_uid == obj.uid
  4212. )
  4213. else:
  4214. raise pagure.exceptions.InvalidObjectException(
  4215. 'Unsupported object found: "%s"' % obj
  4216. )
  4217. project_watchers_query = session.query(model.Watcher).filter(
  4218. model.Watcher.project_id == obj.project.id
  4219. )
  4220. users = set()
  4221. # Add the person who opened the object
  4222. users.add(obj.user.username)
  4223. # Add all the people who commented on that object
  4224. for comment in obj.comments:
  4225. users.add(comment.user.username)
  4226. # Add the user of the project
  4227. users.add(obj.project.user.username)
  4228. # Add the assignee if there is one
  4229. if obj.assignee:
  4230. users.add(obj.assignee.username)
  4231. # Add the regular contributors
  4232. for contributor in obj.project.users:
  4233. users.add(contributor.username)
  4234. # Add people in groups with commit access
  4235. for group in obj.project.groups:
  4236. for member in group.users:
  4237. users.add(member.username)
  4238. # If the issue isn't private:
  4239. if not private:
  4240. # Add all the people watching the repo, remove those who opted-out
  4241. for watcher in project_watchers_query.all():
  4242. if watcher.watch_issues:
  4243. users.add(watcher.user.username)
  4244. else:
  4245. if watcher.user.username in users:
  4246. users.remove(watcher.user.username)
  4247. # Add all the people watching this object, remove those who opted-out
  4248. for watcher in obj_watchers_query.all():
  4249. if watcher.watch:
  4250. users.add(watcher.user.username)
  4251. else:
  4252. if watcher.user.username in users:
  4253. users.remove(watcher.user.username)
  4254. return users
  4255. def save_report(session, repo, name, url, username):
  4256. """Save the report of issues based on the given URL of the project."""
  4257. url_obj = urlparse(url)
  4258. url = url_obj.geturl().replace(url_obj.query, "")
  4259. query = {}
  4260. for k, v in parse_qsl(url_obj.query):
  4261. if k in query:
  4262. if isinstance(query[k], list):
  4263. query[k].append(v)
  4264. else:
  4265. query[k] = [query[k], v]
  4266. else:
  4267. query[k] = v
  4268. reports = repo.reports
  4269. reports[name] = query
  4270. repo.reports = reports
  4271. session.add(repo)
  4272. def set_custom_key_fields(session, project, fields, types, data, notify=None):
  4273. """Set or update the custom key fields of a project with the values
  4274. provided. "data" is currently only used for lists and dates
  4275. """
  4276. current_keys = {}
  4277. for key in project.issue_keys:
  4278. current_keys[key.name] = key
  4279. for idx, key in enumerate(fields):
  4280. if types[idx] == "list":
  4281. if data[idx]:
  4282. data[idx] = [item.strip() for item in data[idx].split(",")]
  4283. elif types[idx] == "date":
  4284. if data[idx]:
  4285. data[idx] = data[idx].strip()
  4286. else:
  4287. data[idx] = None
  4288. if notify and notify[idx] == "on":
  4289. notify_flag = True
  4290. else:
  4291. notify_flag = False
  4292. if key in current_keys:
  4293. issuekey = current_keys[key]
  4294. issuekey.key_type = types[idx]
  4295. issuekey.data = data[idx]
  4296. issuekey.key_notify = notify_flag
  4297. else:
  4298. issuekey = model.IssueKeys(
  4299. project_id=project.id,
  4300. name=key,
  4301. key_type=types[idx],
  4302. data=data[idx],
  4303. key_notify=notify_flag,
  4304. )
  4305. session.add(issuekey)
  4306. # Delete keys
  4307. for key in current_keys:
  4308. if key not in fields:
  4309. session.delete(current_keys[key])
  4310. return "List of custom fields updated"
  4311. def set_custom_key_value(session, issue, key, value):
  4312. """Set or update the value of the specified custom key."""
  4313. query = (
  4314. session.query(model.IssueValues)
  4315. .filter(model.IssueValues.key_id == key.id)
  4316. .filter(model.IssueValues.issue_uid == issue.uid)
  4317. )
  4318. current_field = query.first()
  4319. updated = False
  4320. delete = False
  4321. old_value = None
  4322. if current_field:
  4323. old_value = current_field.value
  4324. if current_field.key.key_type == "boolean":
  4325. value = value or False
  4326. if value is None or value == "":
  4327. session.delete(current_field)
  4328. updated = True
  4329. delete = True
  4330. elif current_field.value != value:
  4331. current_field.value = value
  4332. updated = True
  4333. else:
  4334. if value is None or value == "":
  4335. delete = True
  4336. else:
  4337. current_field = model.IssueValues(
  4338. issue_uid=issue.uid, key_id=key.id, value=value
  4339. )
  4340. updated = True
  4341. if not delete:
  4342. session.add(current_field)
  4343. if REDIS and updated:
  4344. if issue.private:
  4345. REDIS.publish(
  4346. "pagure.%s" % issue.uid,
  4347. json.dumps({"issue": "private", "custom_fields": [key.name]}),
  4348. )
  4349. else:
  4350. REDIS.publish(
  4351. "pagure.%s" % issue.uid,
  4352. json.dumps(
  4353. {
  4354. "custom_fields": [key.name],
  4355. "issue": issue.to_json(
  4356. public=True, with_comments=False
  4357. ),
  4358. }
  4359. ),
  4360. )
  4361. if updated and value:
  4362. output = "Custom field %s adjusted to %s" % (key.name, value)
  4363. if old_value:
  4364. output += " (was: %s)" % old_value
  4365. return output
  4366. elif updated and old_value:
  4367. return "Custom field %s reset (from %s)" % (key.name, old_value)
  4368. def get_yearly_stats_user(session, user, date, tz="UTC"):
  4369. """Return the activity of the specified user in the year preceding the
  4370. specified date. 'offset' is intended to be a timezone offset from UTC,
  4371. in minutes: you can discover the offset for a timezone and pass that
  4372. in order for the results to be relative to that timezone. Note, offset
  4373. should be the amount of minutes that should be added to the UTC time to
  4374. produce the local time - so for timezones behind UTC the number should
  4375. be negative, and for timezones ahead of UTC the number should be
  4376. positive. This is the opposite of what Javascript getTimezoneOffset()
  4377. does, so you have to invert any value you get from that.
  4378. """
  4379. start_date = datetime.datetime(date.year - 1, date.month, date.day)
  4380. events = (
  4381. session.query(model.PagureLog)
  4382. .filter(model.PagureLog.date_created.between(start_date, date))
  4383. .filter(model.PagureLog.user_id == user.id)
  4384. .all()
  4385. )
  4386. # Counter very handily does exactly what we want here: it gives
  4387. # us a dict with the dates as keys and the number of times each
  4388. # date occurs in the data as the values, we return its items as
  4389. # a list of tuples
  4390. return list(Counter([event.date_tz(tz) for event in events]).items())
  4391. def get_user_activity_day(session, user, date, tz="UTC"):
  4392. """Return the activity of the specified user on the specified date.
  4393. 'offset' is intended to be a timezone offset from UTC, in minutes:
  4394. you can discover the offset for a timezone and pass that, so this
  4395. will return activity that occurred on the specified date in the
  4396. desired timezone. Note, offset should be the amount of minutes
  4397. that should be added to the UTC time to produce the local time -
  4398. so for timezones behind UTC the number should be negative, and
  4399. for timezones ahead of UTC the number should be positive. This is
  4400. the opposite of what Javascript getTimezoneOffset() does, so you
  4401. have to invert any value you get from that.
  4402. """
  4403. dt = datetime.datetime.strptime(date, "%Y-%m-%d")
  4404. # if the offset is *negative* some of the events we want may be
  4405. # on the next day in UTC terms. if the offset is *positive* some
  4406. # of the events we want may be on the previous day in UTC terms.
  4407. # 'dt' will be at 00:00, so we subtract 1 day for prevday but add
  4408. # 2 days for nextday. e.g. 2018-02-15 00:00 - prevday will be
  4409. # 2018-02-14 00:00, nextday will be 2018-02-17 00:00. We'll get
  4410. # all events that occurred on 2018-02-14, 2018-02-15 or 2018-02-16
  4411. # in UTC time.
  4412. prevday = dt - datetime.timedelta(days=1)
  4413. nextday = dt + datetime.timedelta(days=2)
  4414. query = (
  4415. session.query(model.PagureLog)
  4416. .filter(model.PagureLog.date_created.between(prevday, nextday))
  4417. .filter(model.PagureLog.user_id == user.id)
  4418. .order_by(model.PagureLog.id.asc())
  4419. )
  4420. events = query.all()
  4421. # Now we filter down to the events that *really* occurred on the
  4422. # date we were asked for with the offset applied, and return
  4423. return [ev for ev in events if ev.date_tz(tz) == dt.date()]
  4424. def get_watchlist_messages(session, user, limit=None):
  4425. watched = user_watch_list(session, user.username)
  4426. watched_list = [watch.id for watch in watched]
  4427. events = (
  4428. session.query(model.PagureLog)
  4429. .filter(model.PagureLog.project_id.in_(watched_list))
  4430. .order_by(model.PagureLog.id.desc())
  4431. )
  4432. if limit is not None:
  4433. events = events.limit(limit)
  4434. events = events.all()
  4435. return events
  4436. def log_action(session, action, obj, user_obj):
  4437. """Log an user action on a project/issue/PR."""
  4438. project_id = None
  4439. if obj.isa in ["issue", "pull-request"]:
  4440. project_id = obj.project_id
  4441. if obj.project.private:
  4442. return
  4443. elif obj.isa == "project":
  4444. project_id = obj.id
  4445. if obj.private:
  4446. return
  4447. else:
  4448. raise pagure.exceptions.InvalidObjectException(
  4449. 'Unsupported object found: "%s"' % obj
  4450. )
  4451. if obj.private:
  4452. return
  4453. log = model.PagureLog(
  4454. user_id=user_obj.id,
  4455. project_id=project_id,
  4456. log_type=action,
  4457. ref_id=obj.id,
  4458. )
  4459. if obj.isa == "issue":
  4460. setattr(log, "issue_uid", obj.uid)
  4461. elif obj.isa == "pull-request":
  4462. setattr(log, "pull_request_uid", obj.uid)
  4463. session.add(log)
  4464. session.commit()
  4465. def email_logs_count(session, email):
  4466. """Returns the number of logs associated with a given email."""
  4467. query = session.query(model.PagureLog).filter(
  4468. model.PagureLog.user_email == email
  4469. )
  4470. return query.count()
  4471. def update_log_email_user(session, email, user):
  4472. """Update the logs with the provided email to point to the specified
  4473. user.
  4474. """
  4475. session.query(model.PagureLog).filter(
  4476. model.PagureLog.user_email == email
  4477. ).update({model.PagureLog.user_id: user.id}, synchronize_session=False)
  4478. def get_custom_key(session, project, keyname):
  4479. """Returns custom key object given it's name and the project"""
  4480. query = (
  4481. session.query(model.IssueKeys)
  4482. .filter(model.IssueKeys.project_id == project.id)
  4483. .filter(model.IssueKeys.name == keyname)
  4484. )
  4485. return query.first()
  4486. def get_active_milestones(session, project):
  4487. """Returns the list of all the active milestones for a given project."""
  4488. query = (
  4489. session.query(model.Issue.milestone)
  4490. .filter(model.Issue.project_id == project.id)
  4491. .filter(model.Issue.status == "Open")
  4492. .filter(model.Issue.milestone.isnot(None))
  4493. )
  4494. return sorted([item[0] for item in query.distinct()])
  4495. def add_metadata_update_notif(session, obj, messages, user):
  4496. """Add a notification to the specified issue with the given messages
  4497. which should reflect changes made to the meta-data of the issue.
  4498. """
  4499. if not messages:
  4500. return
  4501. if not isinstance(messages, (list, set)):
  4502. messages = [messages]
  4503. user_id = None
  4504. if user:
  4505. user_obj = get_user(session, user)
  4506. user_id = user_obj.id
  4507. if obj.isa == "issue":
  4508. obj_comment = model.IssueComment(
  4509. issue_uid=obj.uid,
  4510. comment="**Metadata Update from @%s**:\n- %s"
  4511. % (user, "\n- ".join(sorted(messages))),
  4512. user_id=user_id,
  4513. notification=True,
  4514. )
  4515. obj.last_updated = datetime.datetime.utcnow()
  4516. elif obj.isa == "pull-request":
  4517. obj_comment = model.PullRequestComment(
  4518. pull_request_uid=obj.uid,
  4519. comment="**Metadata Update from @%s**:\n- %s"
  4520. % (user, "\n- ".join(sorted(messages))),
  4521. user_id=user_id,
  4522. notification=True,
  4523. )
  4524. obj.updated_on = datetime.datetime.utcnow()
  4525. session.add(obj)
  4526. session.add(obj_comment)
  4527. # Make sure we won't have SQLAlchemy error before we continue
  4528. session.commit()
  4529. if REDIS:
  4530. REDIS.publish(
  4531. "pagure.%s" % obj.uid,
  4532. json.dumps(
  4533. {
  4534. "comment_id": obj_comment.id,
  4535. "%s_id" % obj.isa: obj.id,
  4536. "project": obj.project.fullname,
  4537. "comment_added": text2markdown(obj_comment.comment),
  4538. "comment_user": obj_comment.user.user,
  4539. "avatar_url": avatar_url_from_email(
  4540. obj_comment.user.default_email, size=16
  4541. ),
  4542. "comment_date": obj_comment.date_created.strftime(
  4543. "%Y-%m-%d %H:%M:%S"
  4544. ),
  4545. "notification": True,
  4546. }
  4547. ),
  4548. )
  4549. pagure.lib.git.update_git(obj, repo=obj.project)
  4550. def tokenize_search_string(pattern):
  4551. """This function tokenizes search patterns into key:value and rest.
  4552. It will also correctly parse key values between quotes.
  4553. """
  4554. if pattern is None:
  4555. return {}, None
  4556. def finalize_token(token, custom_search):
  4557. if ":" in token:
  4558. # This was a "key:value" parameter
  4559. key, value = token.split(":", 1)
  4560. custom_search[key] = value
  4561. return ""
  4562. else:
  4563. # This was a token without colon, thus a search pattern
  4564. return "%s " % token
  4565. custom_search = {}
  4566. # Remaining is the remaining real search_pattern (aka, non-key:values)
  4567. remaining = ""
  4568. # Token is the current "search token" we are processing
  4569. token = ""
  4570. in_quotes = False
  4571. for char in pattern:
  4572. if char == " " and not in_quotes:
  4573. remaining += finalize_token(token, custom_search)
  4574. token = ""
  4575. elif char == '"':
  4576. in_quotes = not in_quotes
  4577. else:
  4578. token += char
  4579. # Parse the final token
  4580. remaining += finalize_token(token, custom_search)
  4581. return custom_search, remaining.strip()
  4582. def get_access_levels(session):
  4583. """Returns all the access levels a user/group can have for a project"""
  4584. access_level_objs = session.query(model.AccessLevels).all()
  4585. return [access_level.access for access_level in access_level_objs]
  4586. def get_obj_access(session, project_obj, obj):
  4587. """Returns the level of access the user/group has on the project.
  4588. :arg session: the session to use to connect to the database.
  4589. :arg project_obj: SQLAlchemy object of Project class
  4590. :arg obj: SQLAlchemy object of either User or PagureGroup class
  4591. """
  4592. if isinstance(obj, model.User):
  4593. query = (
  4594. session.query(model.ProjectUser)
  4595. .filter(model.ProjectUser.project_id == project_obj.id)
  4596. .filter(model.ProjectUser.user_id == obj.id)
  4597. )
  4598. else:
  4599. query = (
  4600. session.query(model.ProjectGroup)
  4601. .filter(model.ProjectGroup.project_id == project_obj.id)
  4602. .filter(model.ProjectGroup.group_id == obj.id)
  4603. )
  4604. return query.first()
  4605. def search_token(
  4606. session,
  4607. acls,
  4608. user=None,
  4609. token=None,
  4610. active=False,
  4611. expired=False,
  4612. description=None,
  4613. ):
  4614. """Searches the API tokens corresponding to the criterias specified.
  4615. :arg session: the session to use to connect to the database.
  4616. :arg acls: List of the ACL associated with these API tokens
  4617. :arg user: restrict the API tokens to this given user
  4618. :arg token: restrict the API tokens to this specified token (if it
  4619. exists)
  4620. :arg description: restrict the API tokens to this given description
  4621. """
  4622. query = (
  4623. session.query(model.Token)
  4624. .filter(model.Token.id == model.TokenAcl.token_id)
  4625. .filter(model.TokenAcl.acl_id == model.ACL.id)
  4626. )
  4627. if acls:
  4628. if isinstance(acls, list):
  4629. query = query.filter(model.ACL.name.in_(acls))
  4630. else:
  4631. query = query.filter(model.ACL.name == acls)
  4632. if user:
  4633. query = query.filter(model.Token.user_id == model.User.id).filter(
  4634. model.User.user == user
  4635. )
  4636. if description:
  4637. query = query.filter(model.Token.description == description)
  4638. if active:
  4639. query = query.filter(
  4640. model.Token.expiration > datetime.datetime.utcnow()
  4641. )
  4642. elif expired:
  4643. query = query.filter(
  4644. model.Token.expiration <= datetime.datetime.utcnow()
  4645. )
  4646. if token:
  4647. query = query.filter(model.Token.id == token)
  4648. return query.first()
  4649. else:
  4650. return query.all()
  4651. def set_project_owner(session, project, user, required_groups=None):
  4652. """Set the ownership of a project
  4653. :arg session: the session to use to connect to the database.
  4654. :arg project: a Project object representing the project's ownership to
  4655. change.
  4656. :arg user: a User object representing the new owner of the project.
  4657. :arg required_groups: a dict of {pattern: [list of groups]} the new user
  4658. should be in to become owner if one of the pattern matches the
  4659. project fullname.
  4660. :return: None
  4661. """
  4662. if required_groups:
  4663. for key in required_groups:
  4664. if fnmatch.fnmatch(project.fullname, key):
  4665. user_grps = set(user.groups)
  4666. req_grps = set(required_groups[key])
  4667. if not user_grps.intersection(req_grps):
  4668. raise pagure.exceptions.PagureException(
  4669. "This user must be in one of the following groups "
  4670. "to be allowed to be added to this project: %s"
  4671. % ", ".join(req_grps)
  4672. )
  4673. for contributor in project.users:
  4674. if user.id == contributor.id:
  4675. project.users.remove(contributor)
  4676. break
  4677. project.user = user
  4678. project.date_modified = datetime.datetime.utcnow()
  4679. session.add(project)
  4680. def get_pagination_metadata(
  4681. flask_request, page, per_page, total, key_page="page"
  4682. ):
  4683. """
  4684. Returns pagination metadata for an API. The code was inspired by
  4685. Flask-SQLAlchemy.
  4686. :param flask_request: flask.request object
  4687. :param page: int of the current page
  4688. :param per_page: int of results per page
  4689. :param total: int of total results
  4690. :param key_page: the name of the argument corresponding to the page
  4691. :return: dictionary of pagination metadata
  4692. """
  4693. pages = int(ceil(total / float(per_page)))
  4694. request_args_wo_page = dict(copy.deepcopy(flask_request.args))
  4695. # Remove pagination related args because those are handled elsewhere
  4696. # Also, remove any args that url_for accepts in case the user entered
  4697. # those in
  4698. for key in [key_page, "per_page", "endpoint"]:
  4699. if key in request_args_wo_page:
  4700. request_args_wo_page.pop(key)
  4701. for key in flask_request.args:
  4702. if key.startswith("_"):
  4703. request_args_wo_page.pop(key)
  4704. request_args_wo_page.update(flask_request.view_args)
  4705. next_page = None
  4706. if page < pages:
  4707. request_args_wo_page.update({key_page: page + 1})
  4708. next_page = url_for(
  4709. flask_request.endpoint,
  4710. per_page=per_page,
  4711. _external=True,
  4712. **request_args_wo_page
  4713. )
  4714. prev_page = None
  4715. if page > 1:
  4716. request_args_wo_page.update({key_page: page - 1})
  4717. prev_page = url_for(
  4718. flask_request.endpoint,
  4719. per_page=per_page,
  4720. _external=True,
  4721. **request_args_wo_page
  4722. )
  4723. request_args_wo_page.update({key_page: 1})
  4724. first_page = url_for(
  4725. flask_request.endpoint,
  4726. per_page=per_page,
  4727. _external=True,
  4728. **request_args_wo_page
  4729. )
  4730. request_args_wo_page.update({key_page: pages})
  4731. last_page = url_for(
  4732. flask_request.endpoint,
  4733. per_page=per_page,
  4734. _external=True,
  4735. **request_args_wo_page
  4736. )
  4737. return {
  4738. key_page: page,
  4739. "pages": pages,
  4740. "per_page": per_page,
  4741. "prev": prev_page,
  4742. "next": next_page,
  4743. "first": first_page,
  4744. "last": last_page,
  4745. }
  4746. def update_star_project(session, repo, star, user):
  4747. """Unset or set the star status depending on the star value.
  4748. :arg session: the session to use to connect to the database.
  4749. :arg repo: a model.Project object representing the project to star/unstar
  4750. :arg star: '1' for starring and '0' for unstarring
  4751. :arg user: string representing the user
  4752. :return: None or string containing 'You starred this project' or
  4753. 'You unstarred this project'
  4754. """
  4755. if not all([repo, user, star]):
  4756. return
  4757. user_obj = get_user(session, user)
  4758. msg = None
  4759. if star == "1":
  4760. msg = _star_project(session, repo=repo, user=user_obj)
  4761. elif star == "0":
  4762. msg = _unstar_project(session, repo=repo, user=user_obj)
  4763. return msg
  4764. def _star_project(session, repo, user):
  4765. """Star a project
  4766. :arg session: Session object to connect to db with
  4767. :arg repo: model.Project object representing the repo to star
  4768. :arg user: model.User object who is starring this repo
  4769. :return: None or string containing 'You starred this project'
  4770. """
  4771. if not all([repo, user]):
  4772. return
  4773. stargazer_obj = model.Star(project_id=repo.id, user_id=user.id)
  4774. session.add(stargazer_obj)
  4775. return "You starred this project"
  4776. def _unstar_project(session, repo, user):
  4777. """Unstar a project
  4778. :arg session: Session object to connect to db with
  4779. :arg repo: model.Project object representing the repo to unstar
  4780. :arg user: model.User object who is unstarring this repo
  4781. :return: None or string containing 'You unstarred this project'
  4782. or 'You never starred the project'
  4783. """
  4784. if not all([repo, user]):
  4785. return
  4786. # First find the stargazer_obj object
  4787. stargazer_obj = _get_stargazer_obj(session, repo, user)
  4788. if isinstance(stargazer_obj, model.Star):
  4789. session.delete(stargazer_obj)
  4790. msg = "You unstarred this project"
  4791. else:
  4792. msg = "You never starred the project"
  4793. return msg
  4794. def _get_stargazer_obj(session, repo, user):
  4795. """Query the db to find stargazer object with given repo and user
  4796. :arg session: Session object to connect to db with
  4797. :arg repo: model.Project object
  4798. :arg user: model.User object
  4799. :return: None or model.Star object
  4800. """
  4801. if not all([repo, user]):
  4802. return
  4803. stargazer_obj = (
  4804. session.query(model.Star)
  4805. .filter(model.Star.project_id == repo.id)
  4806. .filter(model.Star.user_id == user.id)
  4807. )
  4808. return stargazer_obj.first()
  4809. def has_starred(session, repo, user):
  4810. """Check if a given user has starred a particular project
  4811. :arg session: The session object to query the db with
  4812. :arg repo: model.Project object for which the star is checked
  4813. :arg user: The username of the user in question
  4814. :return: True if user has starred the project, False otherwise
  4815. """
  4816. if not all([repo, user]):
  4817. return
  4818. user_obj = search_user(session, username=user)
  4819. stargazer_obj = _get_stargazer_obj(session, repo, user_obj)
  4820. if isinstance(stargazer_obj, model.Star):
  4821. return True
  4822. return False
  4823. def update_read_only_mode(session, repo, read_only=True):
  4824. """Remove the read only mode from the project
  4825. :arg session: The session object to query the db with
  4826. :arg repo: model.Project object to mark/unmark read only
  4827. :arg read_only: True if project is to be made read only,
  4828. False otherwise
  4829. """
  4830. if (
  4831. not repo
  4832. or not isinstance(repo, model.Project)
  4833. or read_only not in [True, False]
  4834. ):
  4835. return
  4836. helper = pagure.lib.git_auth.get_git_auth_helper()
  4837. if helper.is_dynamic and read_only:
  4838. # No need to set a readonly flag if a dynamic auth backend is in use
  4839. return
  4840. if repo.read_only != read_only:
  4841. repo.read_only = read_only
  4842. session.add(repo)
  4843. def issues_history_stats(session, project, detailed=False, weeks_range=53):
  4844. """Returns the number of opened issues on the specified project over
  4845. the last 365 days
  4846. :arg session: The session object to query the db with
  4847. :arg repo: model.Project object to get the issues stats about
  4848. """
  4849. tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1)
  4850. current_open = (
  4851. session.query(model.Issue)
  4852. .filter(model.Issue.project_id == project.id)
  4853. .filter(model.Issue.status == "Open")
  4854. .count()
  4855. )
  4856. # Check if the oldest ticket with a closed_at date is older than a year
  4857. # ago, if it is, we will assume that all tickets that were closed last year
  4858. # have a closed_at date set.
  4859. oldest_closed = (
  4860. session.query(model.Issue)
  4861. .filter(model.Issue.project_id == project.id)
  4862. .filter(model.Issue.closed_at != None) # noqa
  4863. .order_by(sqlalchemy.asc(model.Issue.closed_at))
  4864. .first()
  4865. )
  4866. a_year_ago = tomorrow - datetime.timedelta(days=(weeks_range * 7))
  4867. if oldest_closed and oldest_closed.closed_at < a_year_ago:
  4868. to_ignore = 0
  4869. else:
  4870. # Some ticket got imported as closed but without a closed_at date, so
  4871. # let's ignore them all
  4872. to_ignore = (
  4873. session.query(model.Issue)
  4874. .filter(model.Issue.project_id == project.id)
  4875. .filter(model.Issue.closed_at == None) # noqa
  4876. .filter(model.Issue.status == "Closed")
  4877. .count()
  4878. )
  4879. # For each week from tomorrow, get the number of open tickets
  4880. output = {}
  4881. for week in range(weeks_range):
  4882. end = tomorrow - datetime.timedelta(days=(week * 7))
  4883. start = end - datetime.timedelta(days=7)
  4884. closed_ticket = (
  4885. session.query(model.Issue)
  4886. .filter(model.Issue.project_id == project.id)
  4887. .filter(model.Issue.closed_at >= start)
  4888. .filter(model.Issue.closed_at < end)
  4889. ).count()
  4890. query_open = (
  4891. session.query(model.Issue)
  4892. .filter(model.Issue.project_id == project.id)
  4893. .filter(model.Issue.date_created >= start)
  4894. .filter(model.Issue.date_created < end)
  4895. )
  4896. # For backward compatibility
  4897. if detailed is False:
  4898. query_open = query_open.filter(model.Issue.status == "Open")
  4899. open_ticket = query_open.count()
  4900. cnt = open_ticket + closed_ticket - to_ignore
  4901. current_open = current_open - open_ticket + closed_ticket
  4902. if current_open < 0:
  4903. current_open = 0
  4904. if cnt < 0:
  4905. cnt = 0
  4906. if detailed is False:
  4907. output[start.isoformat()] = cnt
  4908. else:
  4909. output[start.isoformat()] = {
  4910. "open_ticket": open_ticket,
  4911. "closed_ticket": closed_ticket,
  4912. "count": current_open,
  4913. }
  4914. return output
  4915. def get_authorized_project(
  4916. session, project_name, user=None, namespace=None, asuser=None
  4917. ):
  4918. """Retrieving the project with user permission constraint
  4919. :arg session: The SQLAlchemy session to use
  4920. :type session: sqlalchemy.orm.session.Session
  4921. :arg project_name: Name of the project on pagure
  4922. :type project_name: String
  4923. :arg user: Pagure username
  4924. :type user: String
  4925. :arg namespace: Pagure namespace
  4926. :type namespace: String
  4927. :arg asuser: Username to check for access
  4928. :type asuser: String
  4929. :return: The project object if project is public or user has
  4930. permissions for the project else it returns None
  4931. :rtype: Project
  4932. """
  4933. repo = _get_project(session, project_name, user, namespace)
  4934. if repo and repo.private and not pagure.utils.is_repo_user(repo, asuser):
  4935. return None
  4936. return repo
  4937. def get_project_family(session, project):
  4938. """Retrieve the family of the specified project, ie: all the forks
  4939. of the main project.
  4940. If the specified project is a fork, let's work our way up the chain
  4941. until we find the main project so we can go down and get all the forks
  4942. and the forks of the forks (but not one level more).
  4943. :arg session: The SQLAlchemy session to use
  4944. :type session: sqlalchemy.orm.session.Session
  4945. :arg project: The project whose family is searched
  4946. :type project: pagure.lib.model.Project
  4947. """
  4948. parent = project
  4949. while parent.is_fork:
  4950. parent = parent.parent
  4951. sub = session.query(sqlalchemy.distinct(model.Project.id)).filter(
  4952. model.Project.parent_id == parent.id
  4953. )
  4954. query = (
  4955. session.query(model.Project)
  4956. .filter(
  4957. sqlalchemy.or_(
  4958. model.Project.parent_id.in_(sub.subquery()),
  4959. model.Project.parent_id == parent.id,
  4960. )
  4961. )
  4962. .filter(model.Project.user_id == model.User.id)
  4963. .order_by(model.User.user)
  4964. )
  4965. return [parent] + query.all()
  4966. def link_pr_issue(session, issue, request, origin="commit"):
  4967. """Associate the specified issue with the specified pull-requets.
  4968. :arg session: The SQLAlchemy session to use
  4969. :type session: sqlalchemy.orm.session.Session
  4970. :arg issue: The issue mentioned in the commits of the pull-requests to
  4971. be associated with
  4972. :type issue: pagure.lib.model.Issue
  4973. :arg request: A pull-request to associate the specified issue with
  4974. :type request: pagure.lib.model.PullRequest
  4975. """
  4976. associated_issues = [iss.uid for iss in request.related_issues]
  4977. if issue.uid not in associated_issues:
  4978. obj = model.PrToIssue(
  4979. pull_request_uid=request.uid, issue_uid=issue.uid, origin=origin
  4980. )
  4981. session.add(obj)
  4982. session.flush()
  4983. def remove_user_of_project(session, user, project, agent):
  4984. """Remove the specified user from the given project.
  4985. :arg session: the session with which to connect to the database.
  4986. :arg user: an pagure.lib.model.User object to remove from the project.
  4987. :arg project: an pagure.lib.model.Project object from which to remove
  4988. the specified user.
  4989. :arg agent: the username of the user performing the action.
  4990. """
  4991. userids = [u.id for u in project.users]
  4992. if user.id not in userids:
  4993. raise pagure.exceptions.PagureException(
  4994. "User does not have any access on the repo"
  4995. )
  4996. for u in project.users:
  4997. if u.id == user.id:
  4998. user = u
  4999. project.users.remove(u)
  5000. break
  5001. # Mark the project as read_only, celery will unmark it
  5002. update_read_only_mode(session, project, read_only=True)
  5003. session.commit()
  5004. pagure.lib.git.generate_gitolite_acls(project=project)
  5005. pagure.lib.notify.log(
  5006. project,
  5007. topic="project.user.removed",
  5008. msg=dict(
  5009. project=project.to_json(public=True),
  5010. removed_user=user.username,
  5011. agent=agent,
  5012. ),
  5013. )
  5014. return "User removed"
  5015. def create_board(session, project, name, active, tag):
  5016. """Create a board on a given project.
  5017. :arg session: the session with which to connect to the database.
  5018. :arg project: the model.Project of the project that is creating the
  5019. board.
  5020. :arg name: the name of the board to create.
  5021. :arg active: a boolean specifying if the board is active or not.
  5022. :arg tag: the name of the tag associated with this board.
  5023. """
  5024. tag_obj = get_colored_tag(session=session, tag=tag, project_id=project.id)
  5025. if not tag_obj:
  5026. raise pagure.exceptions.PagureException(
  5027. "No tag found with the name %s" % tag
  5028. )
  5029. board = model.Board(
  5030. project_id=project.id, name=name, active=active, tag_id=tag_obj.id
  5031. )
  5032. session.add(board)
  5033. return board
  5034. def edit_board(session, project, name, active, tag, bg_color=None):
  5035. """Edit an existing board on a given project.
  5036. :arg session: the session with which to connect to the database.
  5037. :arg project: the model.Project of the project that is creating the
  5038. board.
  5039. :arg name: the name of the board to create.
  5040. :arg active: a boolean specifying if the board is active or not.
  5041. :arg tag: the name of the tag associated with this board.
  5042. """
  5043. tag_obj = get_colored_tag(session=session, tag=tag, project_id=project.id)
  5044. if not tag_obj:
  5045. raise pagure.exceptions.PagureException(
  5046. "No tag found with the name %s" % tag
  5047. )
  5048. board_obj = None
  5049. for board in project.boards:
  5050. if board.name == name:
  5051. board_obj = board
  5052. break
  5053. if not board_obj:
  5054. raise pagure.exceptions.PagureException(
  5055. 'Could not find the board "%s"' % name
  5056. )
  5057. board.active = active
  5058. board.tag_id = tag_obj.id
  5059. if bg_color:
  5060. board.bg_colar = bg_color
  5061. session.add(board)
  5062. return board
  5063. def delete_board(session, project, names):
  5064. """Delete boards of a given project.
  5065. :arg session: the session with which to connect to the database.
  5066. :arg project: the model.Project of the project that is creating the
  5067. board.
  5068. :arg names: a list of the name of the boards to remove.
  5069. """
  5070. for name in names:
  5071. board_obj = None
  5072. for board in project.boards:
  5073. if board.name == name:
  5074. board_obj = board
  5075. break
  5076. if board_obj:
  5077. session.delete(board_obj)
  5078. def update_board_status(
  5079. session, board, name, rank, default, close, close_status, bg_color
  5080. ):
  5081. """Create or update the board statuses of a project.
  5082. :arg session: the session with which to connect to the database.
  5083. :arg board: the model.Board of the board being updated.
  5084. :arg name: the name of the status.
  5085. :arg rank: the position of the status on the board, the lower the value,
  5086. the more to the left the status.
  5087. :arg default: whether tickets are added to this status by default.
  5088. :arg close: boolean indicating if ticket reaching this status should
  5089. be closed or not
  5090. :arg close_status: the close_status ticket reaching this status should
  5091. be close as.
  5092. :arg bg_color: the background color of the status on the board.
  5093. """
  5094. status = (
  5095. session.query(model.BoardStatus)
  5096. .filter(model.BoardStatus.board_id == board.id)
  5097. .filter(model.BoardStatus.name == name)
  5098. .first()
  5099. )
  5100. if status:
  5101. status.default = default
  5102. status.rank = rank
  5103. status.bg_color = bg_color
  5104. status.close = close
  5105. status.close_status = close_status
  5106. else:
  5107. status = model.BoardStatus(
  5108. board_id=board.id,
  5109. name=name,
  5110. rank=rank,
  5111. default=default,
  5112. bg_color=bg_color,
  5113. close=close,
  5114. close_status=close_status,
  5115. )
  5116. session.add(status)
  5117. return status
  5118. def add_issue_to_boards(
  5119. session, issue, board_name, user, status_id=None, rank=None
  5120. ):
  5121. """Add the given issue to the boards specified.
  5122. :arg session: the session with which to connect to the database.
  5123. :arg issue: the model.Issue of the issue to add to the boards.
  5124. :arg board_names: a list of board name to which the issue should be added.
  5125. :arg user: the username of the user performing the action
  5126. :kwarg rank: the rank of the issue on the status.
  5127. :kwarg rank: the rank of the issue on the status.
  5128. """
  5129. get_user(session, user)
  5130. board_obj = None
  5131. for board in issue.project.boards:
  5132. if board.name == board_name:
  5133. board_obj = board
  5134. break
  5135. if not board_obj:
  5136. _log.info(
  5137. "Could not add issue %s to %s : board not found", issue, board_name
  5138. )
  5139. raise pagure.exceptions.PagureException("Board not found")
  5140. status = (
  5141. session.query(model.BoardStatus)
  5142. .filter(model.BoardStatus.id == status_id)
  5143. .first()
  5144. )
  5145. if not status:
  5146. status = board.default_status
  5147. if not status:
  5148. raise pagure.exceptions.PagureException(
  5149. "No status provided or default status found"
  5150. )
  5151. if rank is None:
  5152. rank = len(status.boards_issues) + 1
  5153. board_issue = model.BoardIssues(
  5154. issue_uid=issue.uid,
  5155. status_id=status.id,
  5156. rank=rank,
  5157. )
  5158. session.add(board_issue)
  5159. def remove_issue_from_boards(session, issue, board_names, user):
  5160. """Remove the given issue from the specified boards.
  5161. :arg session: the session with which to connect to the database.
  5162. :arg issue: the model.Issue of the issue to add to the boards.
  5163. :arg board_names: a list of board name to which the issue should be added.
  5164. :arg user: the username of the user performing the action
  5165. """
  5166. get_user(session, user)
  5167. for board_issue in issue.boards_issues:
  5168. if board_issue.board.active and board_issue.board.name in board_names:
  5169. _log.debug("Removing %s from %s", board_issue, issue)
  5170. session.delete(board_issue)
  5171. def update_ticket_board_status(
  5172. session,
  5173. board,
  5174. user,
  5175. rank=None,
  5176. status_name=None,
  5177. ticket_uid=None,
  5178. ticket_id=None,
  5179. ):
  5180. """Set the status of a ticket on a given board."""
  5181. if not ticket_uid and not ticket_id:
  5182. raise pagure.exceptions.PagureException(
  5183. "One of ticket_id/ticket_uid must be provided"
  5184. )
  5185. if ticket_uid:
  5186. _log.debug(
  5187. "Looking to add ticket %s to board %s with status %s (rank %s)",
  5188. ticket_uid,
  5189. board.name,
  5190. status_name,
  5191. rank,
  5192. )
  5193. ticket = get_issue_by_uid(session, ticket_uid)
  5194. else:
  5195. _log.debug(
  5196. "Looking to add ticket %s to board %s with status %s (rank %s)",
  5197. ticket_id,
  5198. board.name,
  5199. status_name,
  5200. rank,
  5201. )
  5202. ticket = search_issues(session, board.project, issueid=ticket_id)
  5203. if not ticket:
  5204. raise pagure.exceptions.PagureException(
  5205. "No ticket found with this identifier"
  5206. )
  5207. _log.debug("Ticket found")
  5208. status = (
  5209. session.query(model.BoardStatus)
  5210. .filter(model.BoardStatus.board_id == board.id)
  5211. .filter(model.BoardStatus.name == status_name)
  5212. .first()
  5213. )
  5214. if not status:
  5215. _log.debug(
  5216. "No status found with %s, using the default status" % status_name
  5217. )
  5218. status = board.default_status
  5219. if not status:
  5220. raise pagure.exceptions.PagureException(
  5221. "No status provided or default status found"
  5222. )
  5223. if rank is None:
  5224. rank = len(status.boards_issues) + 1
  5225. comments = []
  5226. seen = []
  5227. if ticket.boards_issues:
  5228. for board_issue in ticket.boards_issues:
  5229. if board_issue.status.board.name == board.name:
  5230. _log.debug("Updating existing board")
  5231. board_issue.status_id = status.id
  5232. board_issue.rank = rank
  5233. session.add(board_issue)
  5234. seen.append(board.name)
  5235. break
  5236. if board.name not in seen:
  5237. _log.debug("Adding to a new board")
  5238. board_issue = model.BoardIssues(
  5239. issue_uid=ticket.uid,
  5240. status_id=status.id,
  5241. rank=rank,
  5242. )
  5243. session.add(board_issue)
  5244. # Flag the ticket if needed
  5245. if board.tag.tag not in ticket.tags_text:
  5246. add_tag_obj(session, obj=ticket, tags=board.tag.tag, user=user)
  5247. comments.append(
  5248. "%s tagged with: %s" % (ticket.isa.capitalize(), board.tag.tag)
  5249. )
  5250. if status.close and ticket.status == "Open":
  5251. _log.debug(
  5252. "Closing ticket %s as the new status on the board is "
  5253. "set to close tickets",
  5254. ticket,
  5255. )
  5256. comments.extend(
  5257. edit_issue(
  5258. session=session,
  5259. issue=ticket,
  5260. user=user,
  5261. status="Closed",
  5262. close_status=status.close_status or None,
  5263. )
  5264. )
  5265. session.add(ticket)
  5266. elif not status.close and ticket.status != "Open":
  5267. comments.extend(
  5268. edit_issue(
  5269. session=session,
  5270. issue=ticket,
  5271. user=user,
  5272. status="Open",
  5273. )
  5274. )
  5275. session.add(ticket)
  5276. if comments:
  5277. add_issue_comment(
  5278. session=session,
  5279. issue=ticket,
  5280. comment="\n".join(comments),
  5281. user=user,
  5282. notification=True,
  5283. )
  5284. def find_warning_characters(repo_obj, commits):
  5285. """Return whether the given list of commits of the specified repository
  5286. object contains forbidden characters or not.
  5287. """
  5288. warn_characters = pagure_config["PR_WARN_CHARACTERS"]
  5289. for commit in commits:
  5290. if commit.parents:
  5291. diff = repo_obj.diff(commit.parents[0], commit)
  5292. else:
  5293. # First commit in the repo
  5294. diff = commit.tree.diff_to_tree(swap=True)
  5295. for patch in diff:
  5296. for hunk in patch.hunks:
  5297. for line in hunk.lines:
  5298. subset = [c for c in line.content if c in warn_characters]
  5299. if subset:
  5300. return True
  5301. return False