test_porcelain.py 210 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791
  1. # test_porcelain.py -- porcelain tests
  2. # Copyright (C) 2013 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for dulwich.porcelain."""
  22. import contextlib
  23. import os
  24. import platform
  25. import re
  26. import shutil
  27. import stat
  28. import subprocess
  29. import sys
  30. import tarfile
  31. import tempfile
  32. import threading
  33. import time
  34. from io import BytesIO, StringIO
  35. from unittest import skipIf
  36. from dulwich import porcelain
  37. from dulwich.diff_tree import tree_changes
  38. from dulwich.errors import CommitError
  39. from dulwich.object_store import DEFAULT_TEMPFILE_GRACE_PERIOD
  40. from dulwich.objects import ZERO_SHA, Blob, Tag, Tree
  41. from dulwich.porcelain import (
  42. CheckoutError, # Hypothetical or real error class
  43. CountObjectsResult,
  44. add,
  45. commit,
  46. )
  47. from dulwich.repo import NoIndexPresent, Repo
  48. from dulwich.server import DictBackend
  49. from dulwich.tests.utils import build_commit_graph, make_commit, make_object
  50. from dulwich.web import make_server, make_wsgi_chain
  51. from . import TestCase
  52. try:
  53. import gpg
  54. except ImportError:
  55. gpg = None
  56. def flat_walk_dir(dir_to_walk):
  57. for dirpath, _, filenames in os.walk(dir_to_walk):
  58. rel_dirpath = os.path.relpath(dirpath, dir_to_walk)
  59. if not dirpath == dir_to_walk:
  60. yield rel_dirpath
  61. for filename in filenames:
  62. if dirpath == dir_to_walk:
  63. yield filename
  64. else:
  65. yield os.path.join(rel_dirpath, filename)
  66. class PorcelainTestCase(TestCase):
  67. def setUp(self) -> None:
  68. super().setUp()
  69. self.test_dir = tempfile.mkdtemp()
  70. self.addCleanup(shutil.rmtree, self.test_dir)
  71. self.repo_path = os.path.join(self.test_dir, "repo")
  72. self.repo = Repo.init(self.repo_path, mkdir=True)
  73. self.addCleanup(self.repo.close)
  74. def assertRecentTimestamp(self, ts) -> None:
  75. # On some slow CIs it does actually take more than 5 seconds to go from
  76. # creating the tag to here.
  77. self.assertLess(time.time() - ts, 50)
  78. @skipIf(gpg is None, "gpg is not available")
  79. class PorcelainGpgTestCase(PorcelainTestCase):
  80. DEFAULT_KEY = """
  81. -----BEGIN PGP PRIVATE KEY BLOCK-----
  82. lQVYBGBjIyIBDADAwydvMPQqeEiK54FG1DHwT5sQejAaJOb+PsOhVa4fLcKsrO3F
  83. g5CxO+/9BHCXAr8xQAtp/gOhDN05fyK3MFyGlL9s+Cd8xf34S3R4rN/qbF0oZmaa
  84. FW0MuGnniq54HINs8KshadVn1Dhi/GYSJ588qNFRl/qxFTYAk+zaGsgX/QgFfy0f
  85. djWXJLypZXu9D6DlyJ0cPSzUlfBkI2Ytx6grzIquRjY0FbkjK3l+iGsQ+ebRMdcP
  86. Sqd5iTN9XuzIUVoBFAZBRjibKV3N2wxlnCbfLlzCyDp7rktzSThzjJ2pVDuLrMAx
  87. 6/L9hIhwmFwdtY4FBFGvMR0b0Ugh3kCsRWr8sgj9I7dUoLHid6ObYhJFhnD3GzRc
  88. U+xX1uy3iTCqJDsG334aQIhC5Giuxln4SUZna2MNbq65ksh38N1aM/t3+Dc/TKVB
  89. rb5KWicRPCQ4DIQkHMDCSPyj+dvRLCPzIaPvHD7IrCfHYHOWuvvPGCpwjo0As3iP
  90. IecoMeguPLVaqgcAEQEAAQAL/i5/pQaUd4G7LDydpbixPS6r9UrfPrU/y5zvBP/p
  91. DCynPDutJ1oq539pZvXQ2VwEJJy7x0UVKkjyMndJLNWly9wHC7o8jkHx/NalVP47
  92. LXR+GWbCdOOcYYbdAWcCNB3zOtzPnWhdAEagkc2G9xRQDIB0dLHLCIUpCbLP/CWM
  93. qlHnDsVMrVTWjgzcpsnyGgw8NeLYJtYGB8dsN+XgCCjo7a9LEvUBKNgdmWBbf14/
  94. iBw7PCugazFcH9QYfZwzhsi3nqRRagTXHbxFRG0LD9Ro9qCEutHYGP2PJ59Nj8+M
  95. zaVkJj/OxWxVOGvn2q16mQBCjKpbWfqXZVVl+G5DGOmiSTZqXy+3j6JCKdOMy6Qd
  96. JBHOHhFZXYmWYaaPzoc33T/C3QhMfY5sOtUDLJmV05Wi4dyBeNBEslYgUuTk/jXb
  97. 5ZAie25eDdrsoqkcnSs2ZguMF7AXhe6il2zVhUUMs/6UZgd6I7I4Is0HXT/pnxEp
  98. uiTRFu4v8E+u+5a8O3pffe5boQYA3TsIxceen20qY+kRaTOkURHMZLn/y6KLW8bZ
  99. rNJyXWS9hBAcbbSGhfOwYfzbDCM17yPQO3E2zo8lcGdRklUdIIaCxQwtu36N5dfx
  100. OLCCQc5LmYdl/EAm91iAhrr7dNntZ18MU09gdzUu+ONZwu4CP3cJT83+qYZULso8
  101. 4Fvd/X8IEfGZ7kM+ylrdqBwtlrn8yYXtom+ows2M2UuNR53B+BUOd73kVLTkTCjE
  102. JH63+nE8BqG7tDLCMws+23SAA3xxBgDfDrr0x7zCozQKVQEqBzQr9Uoo/c/ZjAfi
  103. syzNSrDz+g5gqJYtuL9XpPJVWf6V1GXVyJlSbxR9CjTkBxmlPxpvV25IsbVSsh0o
  104. aqkf2eWpbCL6Qb2E0jd1rvf8sGeTTohzYfiSVVsC2t9ngRO/CmetizwQBvRzLGMZ
  105. 4mtAPiy7ZEDc2dFrPp7zlKISYmJZUx/DJVuZWuOrVMpBP+bSgJXoMTlICxZUqUnE
  106. 2VKVStb/L+Tl8XCwIWdrZb9BaDnHqfcGAM2B4HNPxP88Yj1tEDly/vqeb3vVMhj+
  107. S1lunnLdgxp46YyuTMYAzj88eCGurRtzBsdxxlGAsioEnZGebEqAHQbieKq/DO6I
  108. MOMZHMSVBDqyyIx3assGlxSX8BSFW0lhKyT7i0XqnAgCJ9f/5oq0SbFGq+01VQb7
  109. jIx9PbcYJORxsE0JG/CXXPv27bRtQXsudkWGSYvC0NLOgk4z8+kQpQtyFh16lujq
  110. WRwMeriu0qNDjCa1/eHIKDovhAZ3GyO5/9m1tBlUZXN0IFVzZXIgPHRlc3RAdGVz
  111. dC5jb20+iQHOBBMBCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEjrR8
  112. MQ4fJK44PYMvfN2AClLmXiYFAmDcEZEACgkQfN2AClLmXibZzgv/ZfeTpTuqQE1W
  113. C1jT5KpQExnt0BizTX0U7BvSn8Fr6VXTyol6kYc3u71GLUuJyawCLtIzOXqOXJvz
  114. bjcZqymcMADuftKcfMy513FhbF6MhdVd6QoeBP6+7/xXOFJCi+QVYF7SQ2h7K1Qm
  115. +yXOiAMgSxhCZQGPBNJLlDUOd47nSIMANvlumFtmLY/1FD7RpG7WQWjeX1mnxNTw
  116. hUU+Yv7GuFc/JprXCIYqHbhWfvXyVtae2ZK4xuVi5eqwA2RfggOVM7drb+CgPhG0
  117. +9aEDDLOZqVi65wK7J73Puo3rFTbPQMljxw5s27rWqF+vB6hhVdJOPNomWy3naPi
  118. k5MW0mhsacASz1WYndpZz+XaQTq/wJF5HUyyeUWJ0vlOEdwx021PHcqSTyfNnkjD
  119. KncrE21t2sxWRsgGDETxIwkd2b2HNGAvveUD0ffFK/oJHGSXjAERFGc3wuiDj3mQ
  120. BvKm4wt4QF9ZMrCdhMAA6ax5kfEUqQR4ntmrJk/khp/mV7TILaI4nQVYBGBjIyIB
  121. DADghIo9wXnRxzfdDTvwnP8dHpLAIaPokgdpyLswqUCixJWiW2xcV6weUjEWwH6n
  122. eN/t1uZYVehbrotxVPla+MPvzhxp6/cmG+2lhzEBOp6zRwnL1wIB6HoKJfpREhyM
  123. c8rLR0zMso1L1bJTyydvnu07a7BWo3VWKjilb0rEZZUSD/2hidx5HxMOJSoidLWe
  124. d/PPuv6yht3NtA4UThlcfldm9G6PbqCdm1kMEKAkq0wVJvhPJ6gEFRNJimgygfUw
  125. MDFXEIhQtxjgdV5Uoz3O5452VLoRsDlgpi3E0WDGj7WXDaO5uSU0T5aJgVgHCP/f
  126. xZhHuQFk2YYIl5nCBpOZyWWI0IKmscTuEwzpkhICQDQFvcMZ5ibsl7wA2P7YTrQf
  127. FDMjjzuaK80GYPfxDFlyKUyLqFt8w/QzsZLDLX7+jxIEpbRAaMw/JsWqm5BMxxbS
  128. 3CIQiS5S3oSKDsNINelqWFfwvLhvlQra8gIxyNTlek25OdgG66BiiX+seH8A/ql+
  129. F+MAEQEAAQAL/1jrNSLjMt9pwo6qFKClVQZP2vf7+sH7v7LeHIDXr3EnYUnVYnOq
  130. B1FU5PspTp/+J9W25DB9CZLx7Gj8qeslFdiuLSOoIBB4RCToB3kAoeTH0DHqW/Gs
  131. hFTrmJkuDp9zpo/ek6SIXJx5rHAyR9KVw0fizQprH2f6PcgLbTWeM61dJuqowmg3
  132. 7eCOyIKv7VQvFqEhYokLD+JNmrvg+Htg0DXGvdjRjAwPf/NezEXpj67a6cHTp1/C
  133. hwp7pevG+3fTxaCJFesl5/TxxtnaBLE8m2uo/S6Hxgn9l0edonroe1QlTjEqGLy2
  134. 7qi2z5Rem+v6GWNDRgvAWur13v8FNdyduHlioG/NgRsU9mE2MYeFsfi3cfNpJQp/
  135. wC9PSCIXrb/45mkS8KyjZpCrIPB9RV/m0MREq01TPom7rstZc4A1pD0Ot7AtUYS3
  136. e95zLyEmeLziPJ9fV4fgPmEudDr1uItnmV0LOskKlpg5sc0hhdrwYoobfkKt2dx6
  137. DqfMlcM1ZkUbLQYA4jwfpFJG4HmYvjL2xCJxM0ycjvMbqFN+4UjgYWVlRfOrm1V4
  138. Op86FjbRbV6OOCNhznotAg7mul4xtzrrTkK8o3YLBeJseDgl4AWuzXtNa9hE0XpK
  139. 9gJoEHUuBOOsamVh2HpXESFyE5CclOV7JSh541TlZKfnqfZYCg4JSbp0UijkawCL
  140. 5bJJUiGGMD9rZUxIAKQO1DvUEzptS7Jl6S3y5sbIIhilp4KfYWbSk3PPu9CnZD5b
  141. LhEQp0elxnb/IL8PBgD+DpTeC8unkGKXUpbe9x0ISI6V1D6FmJq/FxNg7fMa3QCh
  142. fGiAyoTm80ZETynj+blRaDO3gY4lTLa3Opubof1EqK2QmwXmpyvXEZNYcQfQ2CCS
  143. GOWUCK8jEQamUPf1PWndZXJUmROI1WukhlL71V/ir6zQeVCv1wcwPwclJPnAe87u
  144. pEklnCYpvsEldwHUX9u0BWzoULIEsi+ddtHmT0KTeF/DHRy0W15jIHbjFqhqckj1
  145. /6fmr7l7kIi/kN4vWe0F/0Q8IXX+cVMgbl3aIuaGcvENLGcoAsAtPGx88SfRgmfu
  146. HK64Y7hx1m+Bo215rxJzZRjqHTBPp0BmCi+JKkaavIBrYRbsx20gveI4dzhLcUhB
  147. kiT4Q7oz0/VbGHS1CEf9KFeS/YOGj57s4yHauSVI0XdP9kBRTWmXvBkzsooB2cKH
  148. hwhUN7iiT1k717CiTNUT6Q/pcPFCyNuMoBBGQTU206JEgIjQvI3f8xMUMGmGVVQz
  149. 9/k716ycnhb2JZ/Q/AyQIeHJiQG2BBgBCAAgAhsMFiEEjrR8MQ4fJK44PYMvfN2A
  150. ClLmXiYFAmDcEa4ACgkQfN2AClLmXiZxxQv/XaMN0hPCygtrQMbCsTNb34JbvJzh
  151. hngPuUAfTbRHrR3YeATyQofNbL0DD3fvfzeFF8qESqvzCSZxS6dYsXPd4MCJTzlp
  152. zYBZ2X0sOrgDqZvqCZKN72RKgdk0KvthdzAxsIm2dfcQOxxowXMxhJEXZmsFpusx
  153. jKJxOcrfVRjXJnh9isY0NpCoqMQ+3k3wDJ3VGEHV7G+A+vFkWfbLJF5huQ96uaH9
  154. Uc+jUsREUH9G82ZBqpoioEN8Ith4VXpYnKdTMonK/+ZcyeraJZhXrvbjnEomKdzU
  155. 0pu4bt1HlLR3dcnpjN7b009MBf2xLgEfQk2nPZ4zzY+tDkxygtPllaB4dldFjBpT
  156. j7Q+t49sWMjmlJUbLlHfuJ7nUUK5+cGjBsWVObAEcyfemHWCTVFnEa2BJslGC08X
  157. rFcjRRcMEr9ct4551QFBHsv3O/Wp3/wqczYgE9itSnGT05w+4vLt4smG+dnEHjRJ
  158. brMb2upTHa+kjktjdO96/BgSnKYqmNmPB/qB
  159. =ivA/
  160. -----END PGP PRIVATE KEY BLOCK-----
  161. """
  162. DEFAULT_KEY_ID = "8EB47C310E1F24AE383D832F7CDD800A52E65E26"
  163. NON_DEFAULT_KEY = """
  164. -----BEGIN PGP PRIVATE KEY BLOCK-----
  165. lQVYBGBjI0ABDADGWBRp+t02emfzUlhrc1psqIhhecFm6Em0Kv33cfDpnfoMF1tK
  166. Yy/4eLYIR7FmpdbFPcDThFNHbXJzBi00L1mp0XQE2l50h/2bDAAgREdZ+NVo5a7/
  167. RSZjauNU1PxW6pnXMehEh1tyIQmV78jAukaakwaicrpIenMiFUN3fAKHnLuFffA6
  168. t0f3LqJvTDhUw/o2vPgw5e6UDQhA1C+KTv1KXVrhJNo88a3hZqCZ76z3drKR411Q
  169. zYgT4DUb8lfnbN+z2wfqT9oM5cegh2k86/mxAA3BYOeQrhmQo/7uhezcgbxtdGZr
  170. YlbuaNDTSBrn10ZoaxLPo2dJe2zWxgD6MpvsGU1w3tcRW508qo/+xoWp2/pDzmok
  171. +uhOh1NAj9zB05VWBz1r7oBgCOIKpkD/LD4VKq59etsZ/UnrYDwKdXWZp7uhshkU
  172. M7N35lUJcR76a852dlMdrgpmY18+BP7+o7M+5ElHTiqQbMuE1nHTg8RgVpdV+tUx
  173. dg6GWY/XHf5asm8AEQEAAQAL/A85epOp+GnymmEQfI3+5D178D//Lwu9n86vECB6
  174. xAHCqQtdjZnXpDp/1YUsL59P8nzgYRk7SoMskQDoQ/cB/XFuDOhEdMSgHaTVlnrj
  175. ktCCq6rqGnUosyolbb64vIfVaSqd/5SnCStpAsnaBoBYrAu4ZmV4xfjDQWwn0q5s
  176. u+r56mD0SkjPgbwk/b3qTVagVmf2OFzUgWwm1e/X+bA1oPag1NV8VS4hZPXswT4f
  177. qhiyqUFOgP6vUBcqehkjkIDIl/54xII7/P5tp3LIZawvIXqHKNTqYPCqaCqCj+SL
  178. vMYDIb6acjescfZoM71eAeHAANeFZzr/rwfBT+dEP6qKmPXNcvgE11X44ZCr04nT
  179. zOV/uDUifEvKT5qgtyJpSFEVr7EXubJPKoNNhoYqq9z1pYU7IedX5BloiVXKOKTY
  180. 0pk7JkLqf3g5fYtXh/wol1owemITJy5V5PgaqZvk491LkI6S+kWC7ANYUg+TDPIW
  181. afxW3E5N1CYV6XDAl0ZihbLcoQYAy0Ky/p/wayWKePyuPBLwx9O89GSONK2pQljZ
  182. yaAgxPQ5/i1vx6LIMg7k/722bXR9W3zOjWOin4eatPM3d2hkG96HFvnBqXSmXOPV
  183. 03Xqy1/B5Tj8E9naLKUHE/OBQEc363DgLLG9db5HfPlpAngeppYPdyWkhzXyzkgS
  184. PylaE5eW3zkdjEbYJ6RBTecTZEgBaMvJNPdWbn//frpP7kGvyiCg5Es+WjLInUZ6
  185. 0sdifcNTCewzLXK80v/y5mVOdJhPBgD5zs9cYdyiQJayqAuOr+He1eMHMVUbm9as
  186. qBmPrst398eBW9ZYF7eBfTSlUf6B+WnvyLKEGsUf/7IK0EWDlzoBuWzWiHjUAY1g
  187. m9eTV2MnvCCCefqCErWwfFo2nWOasAZA9sKD+ICIBY4tbtvSl4yfLBzTMwSvs9ZS
  188. K1ocPSYUnhm2miSWZ8RLZPH7roHQasNHpyq/AX7DahFf2S/bJ+46ZGZ8Pigr7hA+
  189. MjmpQ4qVdb5SaViPmZhAKO+PjuCHm+EF/2H0Y3Sl4eXgxZWoQVOUeXdWg9eMfYrj
  190. XDtUMIFppV/QxbeztZKvJdfk64vt/crvLsOp0hOky9cKwY89r4QaHfexU3qR+qDq
  191. UlMvR1rHk7dS5HZAtw0xKsFJNkuDxvBkMqv8Los8zp3nUl+U99dfZOArzNkW38wx
  192. FPa0ixkC9za2BkDrWEA8vTnxw0A2upIFegDUhwOByrSyfPPnG3tKGeqt3Izb/kDk
  193. Q9vmo+HgxBOguMIvlzbBfQZwtbd/gXzlvPqCtCJBbm90aGVyIFRlc3QgVXNlciA8
  194. dGVzdDJAdGVzdC5jb20+iQHOBBMBCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B
  195. AheAFiEEapM5P1DF5qzT1vtFuTYhLttOFMAFAmDcEeEACgkQuTYhLttOFMDe0Qv/
  196. Qx/bzXztJ3BCc+CYAVDx7Kr37S68etwwLgcWzhG+CDeMB5F/QE+upKgxy2iaqQFR
  197. mxfOMgf/TIQkUfkbaASzK1LpnesYO85pk7XYjoN1bYEHiXTkeW+bgB6aJIxrRmO2
  198. SrWasdBC/DsI3Mrya8YMt/TiHC6VpRJVxCe5vv7/kZC4CXrgTBnZocXx/YXimbke
  199. poPMVdbvhYh6N0aGeS38jRKgyN10KXmhDTAQDwseVFavBWAjVfx3DEwjtK2Z2GbA
  200. aL8JvAwRtqiPFkDMIKPL4UwxtXFws8SpMt6juroUkNyf6+BxNWYqmwXHPy8zCJAb
  201. xkxIJMlEc+s7qQsP3fILOo8Xn+dVzJ5sa5AoARoXm1GMjsdqaKAzq99Dic/dHnaQ
  202. Civev1PQsdwlYW2C2wNXNeIrxMndbDMFfNuZ6BnGHWJ/wjcp/pFs4YkyyZN8JH7L
  203. hP2FO4Jgham3AuP13kC3Ivea7V6hR8QNcDZRwFPOMIX4tXwQv1T72+7DZGaA25O7
  204. nQVXBGBjI0ABDADJMBYIcG0Yil9YxFs7aYzNbd7alUAr89VbY8eIGPHP3INFPM1w
  205. lBQCu+4j6xdEbhMpppLBZ9A5TEylP4C6qLtPa+oLtPeuSw8gHDE10XE4lbgPs376
  206. rL60XdImSOHhiduACUefYjqpcmFH9Bim1CC+koArYrSQJQx1Jri+OpnTaL/8UID0
  207. KzD/kEgMVGlHIVj9oJmb4+j9pW8I/g0wDSnIaEKFMxqu6SIVJ1GWj+MUMvZigjLC
  208. sNCZd7PnbOC5VeU3SsXj6he74Jx0AmGMPWIHi9M0DjHO5d1cCbXTnud8xxM1bOh4
  209. 7aCTnMK5cVyIr+adihgJpVVhrndSM8aklBPRgtozrGNCgF2CkYU2P1blxfloNr/8
  210. UZpM83o+s1aObBszzRNLxnpNORqoLqjfPtLEPQnagxE+4EapCq0NZ/x6yO5VTwwp
  211. NljdFAEk40uGuKyn1QA3uNMHy5DlpLl+tU7t1KEovdZ+OVYsYKZhVzw0MTpKogk9
  212. JI7AN0q62ronPskAEQEAAQAL+O8BUSt1ZCVjPSIXIsrR+ZOSkszZwgJ1CWIoh0IH
  213. YD2vmcMHGIhFYgBdgerpvhptKhaw7GcXDScEnYkyh5s4GE2hxclik1tbj/x1gYCN
  214. 8BNoyeDdPFxQG73qN12D99QYEctpOsz9xPLIDwmL0j1ehAfhwqHIAPm9Ca+i8JYM
  215. x/F+35S/jnKDXRI+NVlwbiEyXKXxxIqNlpy9i8sDBGexO5H5Sg0zSN/B1duLekGD
  216. biDw6gLc6bCgnS+0JOUpU07Z2fccMOY9ncjKGD2uIb/ePPUaek92GCQyq0eorCIV
  217. brcQsRc5sSsNtnRKQTQtxioROeDg7kf2oWySeHTswlXW/219ihrSXgteHJd+rPm7
  218. DYLEeGLRny8bRKv8rQdAtApHaJE4dAATXeY4RYo4NlXHYaztGYtU6kiM/3zCfWAe
  219. 9Nn+Wh9jMTZrjefUCagS5r6ZqAh7veNo/vgIGaCLh0a1Ypa0Yk9KFrn3LYEM3zgk
  220. 3m3bn+7qgy5cUYXoJ3DGJJEhBgDPonpW0WElqLs5ZMem1ha85SC38F0IkAaSuzuz
  221. v3eORiKWuyJGF32Q2XHa1RHQs1JtUKd8rxFer3b8Oq71zLz6JtVc9dmRudvgcJYX
  222. 0PC11F6WGjZFSSp39dajFp0A5DKUs39F3w7J1yuDM56TDIN810ywufGAHARY1pZb
  223. UJAy/dTqjFnCbNjpAakor3hVzqxcmUG+7Y2X9c2AGncT1MqAQC3M8JZcuZvkK8A9
  224. cMk8B914ryYE7VsZMdMhyTwHmykGAPgNLLa3RDETeGeGCKWI+ZPOoU0ib5JtJZ1d
  225. P3tNwfZKuZBZXKW9gqYqyBa/qhMip84SP30pr/TvulcdAFC759HK8sQZyJ6Vw24P
  226. c+5ssRxrQUEw1rvJPWhmQCmCOZHBMQl5T6eaTOpR5u3aUKTMlxPKhK9eC1dCSTnI
  227. /nyL8An3VKnLy+K/LI42YGphBVLLJmBewuTVDIJviWRdntiG8dElyEJMOywUltk3
  228. 2CEmqgsD9tPO8rXZjnMrMn3gfsiaoQYA6/6/e2utkHr7gAoWBgrBBdqVHsvqh5Ro
  229. 2DjLAOpZItO/EdCJfDAmbTYOa04535sBDP2tcH/vipPOPpbr1Y9Y/mNsKCulNxed
  230. yqAmEkKOcerLUP5UHju0AB6VBjHJFdU2mqT+UjPyBk7WeKXgFomyoYMv3KpNOFWR
  231. xi0Xji4kKHbttA6Hy3UcGPr9acyUAlDYeKmxbSUYIPhw32bbGrX9+F5YriTufRsG
  232. 3jftQVo9zqdcQSD/5pUTMn3EYbEcohYB2YWJAbYEGAEIACACGwwWIQRqkzk/UMXm
  233. rNPW+0W5NiEu204UwAUCYNwR6wAKCRC5NiEu204UwOPnC/92PgB1c3h9FBXH1maz
  234. g29fndHIHH65VLgqMiQ7HAMojwRlT5Xnj5tdkCBmszRkv5vMvdJRa3ZY8Ed/Inqr
  235. hxBFNzpjqX4oj/RYIQLKXWWfkTKYVLJFZFPCSo00jesw2gieu3Ke/Yy4gwhtNodA
  236. v+s6QNMvffTW/K3XNrWDB0E7/LXbdidzhm+MBu8ov2tuC3tp9liLICiE1jv/2xT4
  237. CNSO6yphmk1/1zEYHS/mN9qJ2csBmte2cdmGyOcuVEHk3pyINNMDOamaURBJGRwF
  238. XB5V7gTKUFU4jCp3chywKrBHJHxGGDUmPBmZtDtfWAOgL32drK7/KUyzZL/WO7Fj
  239. akOI0hRDFOcqTYWL20H7+hAiX3oHMP7eou3L5C7wJ9+JMcACklN/WMjG9a536DFJ
  240. 4UgZ6HyKPP+wy837Hbe8b25kNMBwFgiaLR0lcgzxj7NyQWjVCMOEN+M55tRCjvL6
  241. ya6JVZCRbMXfdCy8lVPgtNQ6VlHaj8Wvnn2FLbWWO2n2r3s=
  242. =9zU5
  243. -----END PGP PRIVATE KEY BLOCK-----
  244. """
  245. NON_DEFAULT_KEY_ID = "6A93393F50C5E6ACD3D6FB45B936212EDB4E14C0"
  246. def setUp(self) -> None:
  247. super().setUp()
  248. self.gpg_dir = os.path.join(self.test_dir, "gpg")
  249. os.mkdir(self.gpg_dir, mode=0o700)
  250. # Ignore errors when deleting GNUPGHOME, because of race conditions
  251. # (e.g. the gpg-agent socket having been deleted). See
  252. # https://github.com/jelmer/dulwich/issues/1000
  253. self.addCleanup(shutil.rmtree, self.gpg_dir, ignore_errors=True)
  254. self.overrideEnv("GNUPGHOME", self.gpg_dir)
  255. def import_default_key(self) -> None:
  256. subprocess.run(
  257. ["gpg", "--import"],
  258. stdout=subprocess.DEVNULL,
  259. stderr=subprocess.DEVNULL,
  260. input=PorcelainGpgTestCase.DEFAULT_KEY,
  261. text=True,
  262. )
  263. def import_non_default_key(self) -> None:
  264. subprocess.run(
  265. ["gpg", "--import"],
  266. stdout=subprocess.DEVNULL,
  267. stderr=subprocess.DEVNULL,
  268. input=PorcelainGpgTestCase.NON_DEFAULT_KEY,
  269. text=True,
  270. )
  271. class ArchiveTests(PorcelainTestCase):
  272. """Tests for the archive command."""
  273. def test_simple(self) -> None:
  274. c1, c2, c3 = build_commit_graph(
  275. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  276. )
  277. self.repo.refs[b"refs/heads/master"] = c3.id
  278. out = BytesIO()
  279. err = BytesIO()
  280. porcelain.archive(
  281. self.repo.path, b"refs/heads/master", outstream=out, errstream=err
  282. )
  283. self.assertEqual(b"", err.getvalue())
  284. tf = tarfile.TarFile(fileobj=out)
  285. self.addCleanup(tf.close)
  286. self.assertEqual([], tf.getnames())
  287. class UpdateServerInfoTests(PorcelainTestCase):
  288. def test_simple(self) -> None:
  289. c1, c2, c3 = build_commit_graph(
  290. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  291. )
  292. self.repo.refs[b"refs/heads/foo"] = c3.id
  293. porcelain.update_server_info(self.repo.path)
  294. self.assertTrue(
  295. os.path.exists(os.path.join(self.repo.controldir(), "info", "refs"))
  296. )
  297. class CommitTests(PorcelainTestCase):
  298. def test_custom_author(self) -> None:
  299. c1, c2, c3 = build_commit_graph(
  300. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  301. )
  302. self.repo.refs[b"refs/heads/foo"] = c3.id
  303. sha = porcelain.commit(
  304. self.repo.path,
  305. message=b"Some message",
  306. author=b"Joe <joe@example.com>",
  307. committer=b"Bob <bob@example.com>",
  308. )
  309. self.assertIsInstance(sha, bytes)
  310. self.assertEqual(len(sha), 40)
  311. def test_unicode(self) -> None:
  312. c1, c2, c3 = build_commit_graph(
  313. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  314. )
  315. self.repo.refs[b"refs/heads/foo"] = c3.id
  316. sha = porcelain.commit(
  317. self.repo.path,
  318. message="Some message",
  319. author="Joe <joe@example.com>",
  320. committer="Bob <bob@example.com>",
  321. )
  322. self.assertIsInstance(sha, bytes)
  323. self.assertEqual(len(sha), 40)
  324. def test_no_verify(self) -> None:
  325. if os.name != "posix":
  326. self.skipTest("shell hook tests requires POSIX shell")
  327. self.assertTrue(os.path.exists("/bin/sh"))
  328. hooks_dir = os.path.join(self.repo.controldir(), "hooks")
  329. os.makedirs(hooks_dir, exist_ok=True)
  330. self.addCleanup(shutil.rmtree, hooks_dir)
  331. c1, c2, c3 = build_commit_graph(
  332. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  333. )
  334. hook_fail = "#!/bin/sh\nexit 1"
  335. # hooks are executed in pre-commit, commit-msg order
  336. # test commit-msg failure first, then pre-commit failure, then
  337. # no_verify to skip both hooks
  338. commit_msg = os.path.join(hooks_dir, "commit-msg")
  339. with open(commit_msg, "w") as f:
  340. f.write(hook_fail)
  341. os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  342. with self.assertRaises(CommitError):
  343. porcelain.commit(
  344. self.repo.path,
  345. message="Some message",
  346. author="Joe <joe@example.com>",
  347. committer="Bob <bob@example.com>",
  348. )
  349. pre_commit = os.path.join(hooks_dir, "pre-commit")
  350. with open(pre_commit, "w") as f:
  351. f.write(hook_fail)
  352. os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  353. with self.assertRaises(CommitError):
  354. porcelain.commit(
  355. self.repo.path,
  356. message="Some message",
  357. author="Joe <joe@example.com>",
  358. committer="Bob <bob@example.com>",
  359. )
  360. sha = porcelain.commit(
  361. self.repo.path,
  362. message="Some message",
  363. author="Joe <joe@example.com>",
  364. committer="Bob <bob@example.com>",
  365. no_verify=True,
  366. )
  367. self.assertIsInstance(sha, bytes)
  368. self.assertEqual(len(sha), 40)
  369. def test_timezone(self) -> None:
  370. c1, c2, c3 = build_commit_graph(
  371. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  372. )
  373. self.repo.refs[b"refs/heads/foo"] = c3.id
  374. sha = porcelain.commit(
  375. self.repo.path,
  376. message="Some message",
  377. author="Joe <joe@example.com>",
  378. author_timezone=18000,
  379. committer="Bob <bob@example.com>",
  380. commit_timezone=18000,
  381. )
  382. self.assertIsInstance(sha, bytes)
  383. self.assertEqual(len(sha), 40)
  384. commit = self.repo.get_object(sha)
  385. self.assertEqual(commit._author_timezone, 18000)
  386. self.assertEqual(commit._commit_timezone, 18000)
  387. self.overrideEnv("GIT_AUTHOR_DATE", "1995-11-20T19:12:08-0501")
  388. self.overrideEnv("GIT_COMMITTER_DATE", "1995-11-20T19:12:08-0501")
  389. sha = porcelain.commit(
  390. self.repo.path,
  391. message="Some message",
  392. author="Joe <joe@example.com>",
  393. committer="Bob <bob@example.com>",
  394. )
  395. self.assertIsInstance(sha, bytes)
  396. self.assertEqual(len(sha), 40)
  397. commit = self.repo.get_object(sha)
  398. self.assertEqual(commit._author_timezone, -18060)
  399. self.assertEqual(commit._commit_timezone, -18060)
  400. self.overrideEnv("GIT_AUTHOR_DATE", None)
  401. self.overrideEnv("GIT_COMMITTER_DATE", None)
  402. local_timezone = time.localtime().tm_gmtoff
  403. sha = porcelain.commit(
  404. self.repo.path,
  405. message="Some message",
  406. author="Joe <joe@example.com>",
  407. committer="Bob <bob@example.com>",
  408. )
  409. self.assertIsInstance(sha, bytes)
  410. self.assertEqual(len(sha), 40)
  411. commit = self.repo.get_object(sha)
  412. self.assertEqual(commit._author_timezone, local_timezone)
  413. self.assertEqual(commit._commit_timezone, local_timezone)
  414. @skipIf(
  415. platform.python_implementation() == "PyPy" or sys.platform == "win32",
  416. "gpgme not easily available or supported on Windows and PyPy",
  417. )
  418. class CommitSignTests(PorcelainGpgTestCase):
  419. def test_default_key(self) -> None:
  420. c1, c2, c3 = build_commit_graph(
  421. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  422. )
  423. self.repo.refs[b"HEAD"] = c3.id
  424. cfg = self.repo.get_config()
  425. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  426. self.import_default_key()
  427. sha = porcelain.commit(
  428. self.repo.path,
  429. message="Some message",
  430. author="Joe <joe@example.com>",
  431. committer="Bob <bob@example.com>",
  432. signoff=True,
  433. )
  434. self.assertIsInstance(sha, bytes)
  435. self.assertEqual(len(sha), 40)
  436. commit = self.repo.get_object(sha)
  437. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  438. commit.verify()
  439. commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
  440. self.import_non_default_key()
  441. self.assertRaises(
  442. gpg.errors.MissingSignatures,
  443. commit.verify,
  444. keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
  445. )
  446. commit.committer = b"Alice <alice@example.com>"
  447. self.assertRaises(
  448. gpg.errors.BadSignatures,
  449. commit.verify,
  450. )
  451. def test_non_default_key(self) -> None:
  452. c1, c2, c3 = build_commit_graph(
  453. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  454. )
  455. self.repo.refs[b"HEAD"] = c3.id
  456. cfg = self.repo.get_config()
  457. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  458. self.import_non_default_key()
  459. sha = porcelain.commit(
  460. self.repo.path,
  461. message="Some message",
  462. author="Joe <joe@example.com>",
  463. committer="Bob <bob@example.com>",
  464. signoff=PorcelainGpgTestCase.NON_DEFAULT_KEY_ID,
  465. )
  466. self.assertIsInstance(sha, bytes)
  467. self.assertEqual(len(sha), 40)
  468. commit = self.repo.get_object(sha)
  469. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  470. commit.verify()
  471. class TimezoneTests(PorcelainTestCase):
  472. def put_envs(self, value) -> None:
  473. self.overrideEnv("GIT_AUTHOR_DATE", value)
  474. self.overrideEnv("GIT_COMMITTER_DATE", value)
  475. def fallback(self, value) -> None:
  476. self.put_envs(value)
  477. self.assertRaises(porcelain.TimezoneFormatError, porcelain.get_user_timezones)
  478. def test_internal_format(self) -> None:
  479. self.put_envs("0 +0500")
  480. self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
  481. def test_rfc_2822(self) -> None:
  482. self.put_envs("Mon, 20 Nov 1995 19:12:08 -0500")
  483. self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
  484. self.put_envs("Mon, 20 Nov 1995 19:12:08")
  485. self.assertTupleEqual((0, 0), porcelain.get_user_timezones())
  486. def test_iso8601(self) -> None:
  487. self.put_envs("1995-11-20T19:12:08-0501")
  488. self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
  489. self.put_envs("1995-11-20T19:12:08+0501")
  490. self.assertTupleEqual((18060, 18060), porcelain.get_user_timezones())
  491. self.put_envs("1995-11-20T19:12:08-05:01")
  492. self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
  493. self.put_envs("1995-11-20 19:12:08-05")
  494. self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
  495. # https://github.com/git/git/blob/96b2d4fa927c5055adc5b1d08f10a5d7352e2989/t/t6300-for-each-ref.sh#L128
  496. self.put_envs("2006-07-03 17:18:44 +0200")
  497. self.assertTupleEqual((7200, 7200), porcelain.get_user_timezones())
  498. def test_missing_or_malformed(self) -> None:
  499. # TODO: add more here
  500. self.fallback("0 + 0500")
  501. self.fallback("a +0500")
  502. self.fallback("1995-11-20T19:12:08")
  503. self.fallback("1995-11-20T19:12:08-05:")
  504. self.fallback("1995.11.20")
  505. self.fallback("11/20/1995")
  506. self.fallback("20.11.1995")
  507. def test_different_envs(self) -> None:
  508. self.overrideEnv("GIT_AUTHOR_DATE", "0 +0500")
  509. self.overrideEnv("GIT_COMMITTER_DATE", "0 +0501")
  510. self.assertTupleEqual((18000, 18060), porcelain.get_user_timezones())
  511. def test_no_envs(self) -> None:
  512. local_timezone = time.localtime().tm_gmtoff
  513. self.put_envs("0 +0500")
  514. self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
  515. self.overrideEnv("GIT_COMMITTER_DATE", None)
  516. self.assertTupleEqual((18000, local_timezone), porcelain.get_user_timezones())
  517. self.put_envs("0 +0500")
  518. self.overrideEnv("GIT_AUTHOR_DATE", None)
  519. self.assertTupleEqual((local_timezone, 18000), porcelain.get_user_timezones())
  520. self.put_envs("0 +0500")
  521. self.overrideEnv("GIT_AUTHOR_DATE", None)
  522. self.overrideEnv("GIT_COMMITTER_DATE", None)
  523. self.assertTupleEqual(
  524. (local_timezone, local_timezone), porcelain.get_user_timezones()
  525. )
  526. class CleanTests(PorcelainTestCase):
  527. def put_files(self, tracked, ignored, untracked, empty_dirs) -> None:
  528. """Put the described files in the wd."""
  529. all_files = tracked | ignored | untracked
  530. for file_path in all_files:
  531. abs_path = os.path.join(self.repo.path, file_path)
  532. # File may need to be written in a dir that doesn't exist yet, so
  533. # create the parent dir(s) as necessary
  534. parent_dir = os.path.dirname(abs_path)
  535. try:
  536. os.makedirs(parent_dir)
  537. except FileExistsError:
  538. pass
  539. with open(abs_path, "w") as f:
  540. f.write("")
  541. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  542. f.writelines(ignored)
  543. for dir_path in empty_dirs:
  544. os.mkdir(os.path.join(self.repo.path, "empty_dir"))
  545. files_to_add = [os.path.join(self.repo.path, t) for t in tracked]
  546. porcelain.add(repo=self.repo.path, paths=files_to_add)
  547. porcelain.commit(repo=self.repo.path, message="init commit")
  548. def assert_wd(self, expected_paths) -> None:
  549. """Assert paths of files and dirs in wd are same as expected_paths."""
  550. control_dir_rel = os.path.relpath(self.repo._controldir, self.repo.path)
  551. # normalize paths to simplify comparison across platforms
  552. found_paths = {
  553. os.path.normpath(p)
  554. for p in flat_walk_dir(self.repo.path)
  555. if not p.split(os.sep)[0] == control_dir_rel
  556. }
  557. norm_expected_paths = {os.path.normpath(p) for p in expected_paths}
  558. self.assertEqual(found_paths, norm_expected_paths)
  559. def test_from_root(self) -> None:
  560. self.put_files(
  561. tracked={"tracked_file", "tracked_dir/tracked_file", ".gitignore"},
  562. ignored={"ignored_file"},
  563. untracked={
  564. "untracked_file",
  565. "tracked_dir/untracked_dir/untracked_file",
  566. "untracked_dir/untracked_dir/untracked_file",
  567. },
  568. empty_dirs={"empty_dir"},
  569. )
  570. porcelain.clean(repo=self.repo.path, target_dir=self.repo.path)
  571. self.assert_wd(
  572. {
  573. "tracked_file",
  574. "tracked_dir/tracked_file",
  575. ".gitignore",
  576. "ignored_file",
  577. "tracked_dir",
  578. }
  579. )
  580. def test_from_subdir(self) -> None:
  581. self.put_files(
  582. tracked={"tracked_file", "tracked_dir/tracked_file", ".gitignore"},
  583. ignored={"ignored_file"},
  584. untracked={
  585. "untracked_file",
  586. "tracked_dir/untracked_dir/untracked_file",
  587. "untracked_dir/untracked_dir/untracked_file",
  588. },
  589. empty_dirs={"empty_dir"},
  590. )
  591. porcelain.clean(
  592. repo=self.repo,
  593. target_dir=os.path.join(self.repo.path, "untracked_dir"),
  594. )
  595. self.assert_wd(
  596. {
  597. "tracked_file",
  598. "tracked_dir/tracked_file",
  599. ".gitignore",
  600. "ignored_file",
  601. "untracked_file",
  602. "tracked_dir/untracked_dir/untracked_file",
  603. "empty_dir",
  604. "untracked_dir",
  605. "tracked_dir",
  606. "tracked_dir/untracked_dir",
  607. }
  608. )
  609. class CloneTests(PorcelainTestCase):
  610. def test_simple_local(self) -> None:
  611. f1_1 = make_object(Blob, data=b"f1")
  612. commit_spec = [[1], [2, 1], [3, 1, 2]]
  613. trees = {
  614. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  615. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  616. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  617. }
  618. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  619. self.repo.refs[b"refs/heads/master"] = c3.id
  620. self.repo.refs[b"refs/tags/foo"] = c3.id
  621. target_path = tempfile.mkdtemp()
  622. errstream = BytesIO()
  623. self.addCleanup(shutil.rmtree, target_path)
  624. r = porcelain.clone(
  625. self.repo.path, target_path, checkout=False, errstream=errstream
  626. )
  627. self.addCleanup(r.close)
  628. self.assertEqual(r.path, target_path)
  629. target_repo = Repo(target_path)
  630. self.assertEqual(0, len(target_repo.open_index()))
  631. self.assertEqual(c3.id, target_repo.refs[b"refs/tags/foo"])
  632. self.assertNotIn(b"f1", os.listdir(target_path))
  633. self.assertNotIn(b"f2", os.listdir(target_path))
  634. c = r.get_config()
  635. encoded_path = self.repo.path
  636. if not isinstance(encoded_path, bytes):
  637. encoded_path = encoded_path.encode("utf-8")
  638. self.assertEqual(encoded_path, c.get((b"remote", b"origin"), b"url"))
  639. self.assertEqual(
  640. b"+refs/heads/*:refs/remotes/origin/*",
  641. c.get((b"remote", b"origin"), b"fetch"),
  642. )
  643. def test_simple_local_with_checkout(self) -> None:
  644. f1_1 = make_object(Blob, data=b"f1")
  645. commit_spec = [[1], [2, 1], [3, 1, 2]]
  646. trees = {
  647. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  648. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  649. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  650. }
  651. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  652. self.repo.refs[b"refs/heads/master"] = c3.id
  653. target_path = tempfile.mkdtemp()
  654. errstream = BytesIO()
  655. self.addCleanup(shutil.rmtree, target_path)
  656. with porcelain.clone(
  657. self.repo.path, target_path, checkout=True, errstream=errstream
  658. ) as r:
  659. self.assertEqual(r.path, target_path)
  660. with Repo(target_path) as r:
  661. self.assertEqual(r.head(), c3.id)
  662. self.assertIn("f1", os.listdir(target_path))
  663. self.assertIn("f2", os.listdir(target_path))
  664. def test_bare_local_with_checkout(self) -> None:
  665. f1_1 = make_object(Blob, data=b"f1")
  666. commit_spec = [[1], [2, 1], [3, 1, 2]]
  667. trees = {
  668. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  669. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  670. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  671. }
  672. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  673. self.repo.refs[b"refs/heads/master"] = c3.id
  674. target_path = tempfile.mkdtemp()
  675. errstream = BytesIO()
  676. self.addCleanup(shutil.rmtree, target_path)
  677. with porcelain.clone(
  678. self.repo.path, target_path, bare=True, errstream=errstream
  679. ) as r:
  680. self.assertEqual(r.path, target_path)
  681. with Repo(target_path) as r:
  682. r.head()
  683. self.assertRaises(NoIndexPresent, r.open_index)
  684. self.assertNotIn(b"f1", os.listdir(target_path))
  685. self.assertNotIn(b"f2", os.listdir(target_path))
  686. def test_no_checkout_with_bare(self) -> None:
  687. f1_1 = make_object(Blob, data=b"f1")
  688. commit_spec = [[1]]
  689. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  690. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  691. self.repo.refs[b"refs/heads/master"] = c1.id
  692. self.repo.refs[b"HEAD"] = c1.id
  693. target_path = tempfile.mkdtemp()
  694. errstream = BytesIO()
  695. self.addCleanup(shutil.rmtree, target_path)
  696. self.assertRaises(
  697. porcelain.Error,
  698. porcelain.clone,
  699. self.repo.path,
  700. target_path,
  701. checkout=True,
  702. bare=True,
  703. errstream=errstream,
  704. )
  705. def test_no_head_no_checkout(self) -> None:
  706. f1_1 = make_object(Blob, data=b"f1")
  707. commit_spec = [[1]]
  708. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  709. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  710. self.repo.refs[b"refs/heads/master"] = c1.id
  711. target_path = tempfile.mkdtemp()
  712. self.addCleanup(shutil.rmtree, target_path)
  713. errstream = BytesIO()
  714. r = porcelain.clone(
  715. self.repo.path, target_path, checkout=True, errstream=errstream
  716. )
  717. r.close()
  718. def test_no_head_no_checkout_outstream_errstream_autofallback(self) -> None:
  719. f1_1 = make_object(Blob, data=b"f1")
  720. commit_spec = [[1]]
  721. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  722. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  723. self.repo.refs[b"refs/heads/master"] = c1.id
  724. target_path = tempfile.mkdtemp()
  725. self.addCleanup(shutil.rmtree, target_path)
  726. errstream = porcelain.NoneStream()
  727. r = porcelain.clone(
  728. self.repo.path, target_path, checkout=True, errstream=errstream
  729. )
  730. r.close()
  731. def test_source_broken(self) -> None:
  732. with tempfile.TemporaryDirectory() as parent:
  733. target_path = os.path.join(parent, "target")
  734. self.assertRaises(
  735. Exception, porcelain.clone, "/nonexistent/repo", target_path
  736. )
  737. self.assertFalse(os.path.exists(target_path))
  738. def test_fetch_symref(self) -> None:
  739. f1_1 = make_object(Blob, data=b"f1")
  740. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  741. [c1] = build_commit_graph(self.repo.object_store, [[1]], trees)
  742. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/else")
  743. self.repo.refs[b"refs/heads/else"] = c1.id
  744. target_path = tempfile.mkdtemp()
  745. errstream = BytesIO()
  746. self.addCleanup(shutil.rmtree, target_path)
  747. r = porcelain.clone(
  748. self.repo.path, target_path, checkout=False, errstream=errstream
  749. )
  750. self.addCleanup(r.close)
  751. self.assertEqual(r.path, target_path)
  752. target_repo = Repo(target_path)
  753. self.assertEqual(0, len(target_repo.open_index()))
  754. self.assertEqual(c1.id, target_repo.refs[b"refs/heads/else"])
  755. self.assertEqual(c1.id, target_repo.refs[b"HEAD"])
  756. self.assertEqual(
  757. {
  758. b"HEAD": b"refs/heads/else",
  759. b"refs/remotes/origin/HEAD": b"refs/remotes/origin/else",
  760. },
  761. target_repo.refs.get_symrefs(),
  762. )
  763. def test_detached_head(self) -> None:
  764. f1_1 = make_object(Blob, data=b"f1")
  765. commit_spec = [[1], [2, 1], [3, 1, 2]]
  766. trees = {
  767. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  768. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  769. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  770. }
  771. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  772. self.repo.refs[b"refs/heads/master"] = c2.id
  773. self.repo.refs.remove_if_equals(b"HEAD", None)
  774. self.repo.refs[b"HEAD"] = c3.id
  775. target_path = tempfile.mkdtemp()
  776. self.addCleanup(shutil.rmtree, target_path)
  777. errstream = porcelain.NoneStream()
  778. with porcelain.clone(
  779. self.repo.path, target_path, checkout=True, errstream=errstream
  780. ) as r:
  781. self.assertEqual(c3.id, r.refs[b"HEAD"])
  782. def test_clone_pathlib(self) -> None:
  783. from pathlib import Path
  784. f1_1 = make_object(Blob, data=b"f1")
  785. commit_spec = [[1]]
  786. trees = {1: [(b"f1", f1_1)]}
  787. c1 = build_commit_graph(self.repo.object_store, commit_spec, trees)[0]
  788. self.repo.refs[b"refs/heads/master"] = c1.id
  789. target_dir = tempfile.mkdtemp()
  790. self.addCleanup(shutil.rmtree, target_dir)
  791. target_path = Path(target_dir) / "clone_repo"
  792. errstream = BytesIO()
  793. r = porcelain.clone(
  794. self.repo.path, target_path, checkout=False, errstream=errstream
  795. )
  796. self.addCleanup(r.close)
  797. self.assertEqual(r.path, str(target_path))
  798. self.assertTrue(os.path.exists(str(target_path)))
  799. class InitTests(TestCase):
  800. def test_non_bare(self) -> None:
  801. repo_dir = tempfile.mkdtemp()
  802. self.addCleanup(shutil.rmtree, repo_dir)
  803. porcelain.init(repo_dir)
  804. def test_bare(self) -> None:
  805. repo_dir = tempfile.mkdtemp()
  806. self.addCleanup(shutil.rmtree, repo_dir)
  807. porcelain.init(repo_dir, bare=True)
  808. def test_init_pathlib(self) -> None:
  809. from pathlib import Path
  810. repo_dir = tempfile.mkdtemp()
  811. self.addCleanup(shutil.rmtree, repo_dir)
  812. repo_path = Path(repo_dir)
  813. # Test non-bare repo with pathlib
  814. repo = porcelain.init(repo_path)
  815. self.assertTrue(os.path.exists(os.path.join(repo_dir, ".git")))
  816. repo.close()
  817. def test_init_bare_pathlib(self) -> None:
  818. from pathlib import Path
  819. repo_dir = tempfile.mkdtemp()
  820. self.addCleanup(shutil.rmtree, repo_dir)
  821. repo_path = Path(repo_dir)
  822. # Test bare repo with pathlib
  823. repo = porcelain.init(repo_path, bare=True)
  824. self.assertTrue(os.path.exists(os.path.join(repo_dir, "refs")))
  825. repo.close()
  826. class AddTests(PorcelainTestCase):
  827. def test_add_default_paths(self) -> None:
  828. # create a file for initial commit
  829. fullpath = os.path.join(self.repo.path, "blah")
  830. with open(fullpath, "w") as f:
  831. f.write("\n")
  832. porcelain.add(repo=self.repo.path, paths=[fullpath])
  833. porcelain.commit(
  834. repo=self.repo.path,
  835. message=b"test",
  836. author=b"test <email>",
  837. committer=b"test <email>",
  838. )
  839. # Add a second test file and a file in a directory
  840. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  841. f.write("\n")
  842. os.mkdir(os.path.join(self.repo.path, "adir"))
  843. with open(os.path.join(self.repo.path, "adir", "afile"), "w") as f:
  844. f.write("\n")
  845. cwd = os.getcwd()
  846. try:
  847. os.chdir(self.repo.path)
  848. self.assertEqual({"foo", "blah", "adir", ".git"}, set(os.listdir(".")))
  849. added, ignored = porcelain.add(self.repo.path)
  850. # Normalize paths to use forward slashes for comparison
  851. added_normalized = [path.replace(os.sep, "/") for path in added]
  852. self.assertEqual(
  853. (added_normalized, ignored),
  854. (["foo", "adir/afile"], set()),
  855. )
  856. finally:
  857. os.chdir(cwd)
  858. # Check that foo was added and nothing in .git was modified
  859. index = self.repo.open_index()
  860. self.assertEqual(sorted(index), [b"adir/afile", b"blah", b"foo"])
  861. def test_add_default_paths_subdir(self) -> None:
  862. os.mkdir(os.path.join(self.repo.path, "foo"))
  863. with open(os.path.join(self.repo.path, "blah"), "w") as f:
  864. f.write("\n")
  865. with open(os.path.join(self.repo.path, "foo", "blie"), "w") as f:
  866. f.write("\n")
  867. cwd = os.getcwd()
  868. try:
  869. os.chdir(os.path.join(self.repo.path, "foo"))
  870. porcelain.add(repo=self.repo.path)
  871. porcelain.commit(
  872. repo=self.repo.path,
  873. message=b"test",
  874. author=b"test <email>",
  875. committer=b"test <email>",
  876. )
  877. finally:
  878. os.chdir(cwd)
  879. index = self.repo.open_index()
  880. # After fix: add() with no paths should behave like git add -A (add everything)
  881. self.assertEqual(sorted(index), [b"blah", b"foo/blie"])
  882. def test_add_file(self) -> None:
  883. fullpath = os.path.join(self.repo.path, "foo")
  884. with open(fullpath, "w") as f:
  885. f.write("BAR")
  886. porcelain.add(self.repo.path, paths=[fullpath])
  887. self.assertIn(b"foo", self.repo.open_index())
  888. def test_add_ignored(self) -> None:
  889. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  890. f.write("foo\nsubdir/")
  891. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  892. f.write("BAR")
  893. with open(os.path.join(self.repo.path, "bar"), "w") as f:
  894. f.write("BAR")
  895. os.mkdir(os.path.join(self.repo.path, "subdir"))
  896. with open(os.path.join(self.repo.path, "subdir", "baz"), "w") as f:
  897. f.write("BAZ")
  898. (added, ignored) = porcelain.add(
  899. self.repo.path,
  900. paths=[
  901. os.path.join(self.repo.path, "foo"),
  902. os.path.join(self.repo.path, "bar"),
  903. os.path.join(self.repo.path, "subdir"),
  904. ],
  905. )
  906. self.assertIn(b"bar", self.repo.open_index())
  907. self.assertEqual({"bar"}, set(added))
  908. self.assertEqual({"foo", "subdir/"}, ignored)
  909. def test_add_file_absolute_path(self) -> None:
  910. # Absolute paths are (not yet) supported
  911. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  912. f.write("BAR")
  913. porcelain.add(self.repo, paths=[os.path.join(self.repo.path, "foo")])
  914. self.assertIn(b"foo", self.repo.open_index())
  915. def test_add_not_in_repo(self) -> None:
  916. with open(os.path.join(self.test_dir, "foo"), "w") as f:
  917. f.write("BAR")
  918. self.assertRaises(
  919. ValueError,
  920. porcelain.add,
  921. self.repo,
  922. paths=[os.path.join(self.test_dir, "foo")],
  923. )
  924. self.assertRaises(
  925. (ValueError, FileNotFoundError),
  926. porcelain.add,
  927. self.repo,
  928. paths=["../foo"],
  929. )
  930. self.assertEqual([], list(self.repo.open_index()))
  931. def test_add_file_clrf_conversion(self) -> None:
  932. # Set the right configuration to the repo
  933. c = self.repo.get_config()
  934. c.set("core", "autocrlf", "input")
  935. c.write_to_path()
  936. # Add a file with CRLF line-ending
  937. fullpath = os.path.join(self.repo.path, "foo")
  938. with open(fullpath, "wb") as f:
  939. f.write(b"line1\r\nline2")
  940. porcelain.add(self.repo.path, paths=[fullpath])
  941. # The line-endings should have been converted to LF
  942. index = self.repo.open_index()
  943. self.assertIn(b"foo", index)
  944. entry = index[b"foo"]
  945. blob = self.repo[entry.sha]
  946. self.assertEqual(blob.data, b"line1\nline2")
  947. def test_add_symlink_outside_repo(self) -> None:
  948. """Test adding a symlink that points outside the repository."""
  949. # Create a symlink pointing outside the repository
  950. symlink_path = os.path.join(self.repo.path, "symlink_to_nowhere")
  951. os.symlink("/nonexistent/path", symlink_path)
  952. # Adding the symlink should succeed (matching Git's behavior)
  953. added, ignored = porcelain.add(self.repo.path, paths=[symlink_path])
  954. # Should successfully add the symlink
  955. self.assertIn("symlink_to_nowhere", added)
  956. self.assertEqual(len(ignored), 0)
  957. # Verify symlink is actually staged
  958. index = self.repo.open_index()
  959. self.assertIn(b"symlink_to_nowhere", index)
  960. def test_add_symlink_to_file_inside_repo(self) -> None:
  961. """Test adding a symlink that points to a file inside the repository."""
  962. # Create a regular file
  963. target_file = os.path.join(self.repo.path, "target.txt")
  964. with open(target_file, "w") as f:
  965. f.write("target content")
  966. # Create a symlink to the file
  967. symlink_path = os.path.join(self.repo.path, "link_to_target")
  968. os.symlink("target.txt", symlink_path)
  969. # Add both the target and the symlink
  970. added, ignored = porcelain.add(
  971. self.repo.path, paths=[target_file, symlink_path]
  972. )
  973. # Both should be added successfully
  974. self.assertIn("target.txt", added)
  975. self.assertIn("link_to_target", added)
  976. self.assertEqual(len(ignored), 0)
  977. # Verify both are in the index
  978. index = self.repo.open_index()
  979. self.assertIn(b"target.txt", index)
  980. self.assertIn(b"link_to_target", index)
  981. def test_add_symlink_to_directory_inside_repo(self) -> None:
  982. """Test adding a symlink that points to a directory inside the repository."""
  983. # Create a directory with some files
  984. target_dir = os.path.join(self.repo.path, "target_dir")
  985. os.mkdir(target_dir)
  986. with open(os.path.join(target_dir, "file1.txt"), "w") as f:
  987. f.write("content1")
  988. with open(os.path.join(target_dir, "file2.txt"), "w") as f:
  989. f.write("content2")
  990. # Create a symlink to the directory
  991. symlink_path = os.path.join(self.repo.path, "link_to_dir")
  992. os.symlink("target_dir", symlink_path)
  993. # Add the symlink
  994. added, ignored = porcelain.add(self.repo.path, paths=[symlink_path])
  995. # When adding a symlink to a directory, it follows the symlink and adds contents
  996. self.assertEqual(len(added), 2)
  997. self.assertIn("link_to_dir/file1.txt", added)
  998. self.assertIn("link_to_dir/file2.txt", added)
  999. self.assertEqual(len(ignored), 0)
  1000. # Verify files are added through the symlink path
  1001. index = self.repo.open_index()
  1002. self.assertIn(b"link_to_dir/file1.txt", index)
  1003. self.assertIn(b"link_to_dir/file2.txt", index)
  1004. # The original target directory files are not added
  1005. self.assertNotIn(b"target_dir/file1.txt", index)
  1006. self.assertNotIn(b"target_dir/file2.txt", index)
  1007. def test_add_symlink_chain(self) -> None:
  1008. """Test adding a chain of symlinks (symlink to symlink)."""
  1009. # Create a regular file
  1010. target_file = os.path.join(self.repo.path, "original.txt")
  1011. with open(target_file, "w") as f:
  1012. f.write("original content")
  1013. # Create first symlink
  1014. first_link = os.path.join(self.repo.path, "link1")
  1015. os.symlink("original.txt", first_link)
  1016. # Create second symlink pointing to first
  1017. second_link = os.path.join(self.repo.path, "link2")
  1018. os.symlink("link1", second_link)
  1019. # Add all files
  1020. added, ignored = porcelain.add(
  1021. self.repo.path, paths=[target_file, first_link, second_link]
  1022. )
  1023. # All should be added
  1024. self.assertEqual(len(added), 3)
  1025. self.assertIn("original.txt", added)
  1026. self.assertIn("link1", added)
  1027. self.assertIn("link2", added)
  1028. # Verify all are in the index
  1029. index = self.repo.open_index()
  1030. self.assertIn(b"original.txt", index)
  1031. self.assertIn(b"link1", index)
  1032. self.assertIn(b"link2", index)
  1033. def test_add_broken_symlink(self) -> None:
  1034. """Test adding a broken symlink (points to non-existent target)."""
  1035. # Create a symlink to a non-existent file
  1036. broken_link = os.path.join(self.repo.path, "broken_link")
  1037. os.symlink("does_not_exist.txt", broken_link)
  1038. # Add the broken symlink
  1039. added, ignored = porcelain.add(self.repo.path, paths=[broken_link])
  1040. # Should be added successfully (Git tracks the symlink, not its target)
  1041. self.assertIn("broken_link", added)
  1042. self.assertEqual(len(ignored), 0)
  1043. # Verify it's in the index
  1044. index = self.repo.open_index()
  1045. self.assertIn(b"broken_link", index)
  1046. def test_add_symlink_relative_outside_repo(self) -> None:
  1047. """Test adding a symlink that uses '..' to point outside the repository."""
  1048. # Create a file outside the repo
  1049. outside_file = os.path.join(self.test_dir, "outside.txt")
  1050. with open(outside_file, "w") as f:
  1051. f.write("outside content")
  1052. # Create a symlink using relative path to go outside
  1053. symlink_path = os.path.join(self.repo.path, "link_outside")
  1054. os.symlink("../outside.txt", symlink_path)
  1055. # Add the symlink
  1056. added, ignored = porcelain.add(self.repo.path, paths=[symlink_path])
  1057. # Should be added successfully
  1058. self.assertIn("link_outside", added)
  1059. self.assertEqual(len(ignored), 0)
  1060. # Verify it's in the index
  1061. index = self.repo.open_index()
  1062. self.assertIn(b"link_outside", index)
  1063. def test_add_symlink_absolute_to_system(self) -> None:
  1064. """Test adding a symlink with absolute path to system directory."""
  1065. # Create a symlink to a system directory
  1066. symlink_path = os.path.join(self.repo.path, "link_to_tmp")
  1067. if os.name == "nt":
  1068. # On Windows, use a system directory like TEMP
  1069. symlink_target = os.environ["TEMP"]
  1070. else:
  1071. # On Unix-like systems, use /tmp
  1072. symlink_target = "/tmp"
  1073. os.symlink(symlink_target, symlink_path)
  1074. # Adding a symlink to a directory outside the repo should raise ValueError
  1075. with self.assertRaises(ValueError) as cm:
  1076. porcelain.add(self.repo.path, paths=[symlink_path])
  1077. # Check that the error indicates the path is outside the repository
  1078. self.assertIn("is not in the subpath of", str(cm.exception))
  1079. def test_add_file_through_symlink(self) -> None:
  1080. """Test adding a file through a symlinked directory."""
  1081. # Create a directory with a file
  1082. real_dir = os.path.join(self.repo.path, "real_dir")
  1083. os.mkdir(real_dir)
  1084. real_file = os.path.join(real_dir, "file.txt")
  1085. with open(real_file, "w") as f:
  1086. f.write("content")
  1087. # Create a symlink to the directory
  1088. link_dir = os.path.join(self.repo.path, "link_dir")
  1089. os.symlink("real_dir", link_dir)
  1090. # Try to add the file through the symlink path
  1091. symlink_file_path = os.path.join(link_dir, "file.txt")
  1092. # This should add the real file, not create a new entry
  1093. added, ignored = porcelain.add(self.repo.path, paths=[symlink_file_path])
  1094. # The real file should be added
  1095. self.assertIn("real_dir/file.txt", added)
  1096. self.assertEqual(len(added), 1)
  1097. # Verify correct path in index
  1098. index = self.repo.open_index()
  1099. self.assertIn(b"real_dir/file.txt", index)
  1100. # Should not create a separate entry for the symlink path
  1101. self.assertNotIn(b"link_dir/file.txt", index)
  1102. def test_add_repo_path(self) -> None:
  1103. """Test adding the repository path itself should add all untracked files."""
  1104. # Create some untracked files
  1105. with open(os.path.join(self.repo.path, "file1.txt"), "w") as f:
  1106. f.write("content1")
  1107. with open(os.path.join(self.repo.path, "file2.txt"), "w") as f:
  1108. f.write("content2")
  1109. # Add the repository path itself
  1110. added, ignored = porcelain.add(self.repo.path, paths=[self.repo.path])
  1111. # Should add all untracked files, not stage './'
  1112. self.assertIn("file1.txt", added)
  1113. self.assertIn("file2.txt", added)
  1114. self.assertNotIn("./", added)
  1115. # Verify files are actually staged
  1116. index = self.repo.open_index()
  1117. self.assertIn(b"file1.txt", index)
  1118. self.assertIn(b"file2.txt", index)
  1119. def test_add_directory_contents(self) -> None:
  1120. """Test adding a directory adds all files within it."""
  1121. # Create a subdirectory with multiple files
  1122. subdir = os.path.join(self.repo.path, "subdir")
  1123. os.mkdir(subdir)
  1124. with open(os.path.join(subdir, "file1.txt"), "w") as f:
  1125. f.write("content1")
  1126. with open(os.path.join(subdir, "file2.txt"), "w") as f:
  1127. f.write("content2")
  1128. with open(os.path.join(subdir, "file3.txt"), "w") as f:
  1129. f.write("content3")
  1130. # Add the directory
  1131. added, ignored = porcelain.add(self.repo.path, paths=["subdir"])
  1132. # Should add all files in the directory
  1133. self.assertEqual(len(added), 3)
  1134. # Normalize paths to use forward slashes for comparison
  1135. added_normalized = [path.replace(os.sep, "/") for path in added]
  1136. self.assertIn("subdir/file1.txt", added_normalized)
  1137. self.assertIn("subdir/file2.txt", added_normalized)
  1138. self.assertIn("subdir/file3.txt", added_normalized)
  1139. # Verify files are actually staged
  1140. index = self.repo.open_index()
  1141. self.assertIn(b"subdir/file1.txt", index)
  1142. self.assertIn(b"subdir/file2.txt", index)
  1143. self.assertIn(b"subdir/file3.txt", index)
  1144. def test_add_nested_directories(self) -> None:
  1145. """Test adding a directory with nested subdirectories."""
  1146. # Create nested directory structure
  1147. dir1 = os.path.join(self.repo.path, "dir1")
  1148. dir2 = os.path.join(dir1, "dir2")
  1149. dir3 = os.path.join(dir2, "dir3")
  1150. os.makedirs(dir3)
  1151. # Add files at each level
  1152. with open(os.path.join(dir1, "file1.txt"), "w") as f:
  1153. f.write("level1")
  1154. with open(os.path.join(dir2, "file2.txt"), "w") as f:
  1155. f.write("level2")
  1156. with open(os.path.join(dir3, "file3.txt"), "w") as f:
  1157. f.write("level3")
  1158. # Add the top-level directory
  1159. added, ignored = porcelain.add(self.repo.path, paths=["dir1"])
  1160. # Should add all files recursively
  1161. self.assertEqual(len(added), 3)
  1162. # Normalize paths to use forward slashes for comparison
  1163. added_normalized = [path.replace(os.sep, "/") for path in added]
  1164. self.assertIn("dir1/file1.txt", added_normalized)
  1165. self.assertIn("dir1/dir2/file2.txt", added_normalized)
  1166. self.assertIn("dir1/dir2/dir3/file3.txt", added_normalized)
  1167. # Verify files are actually staged
  1168. index = self.repo.open_index()
  1169. self.assertIn(b"dir1/file1.txt", index)
  1170. self.assertIn(b"dir1/dir2/file2.txt", index)
  1171. self.assertIn(b"dir1/dir2/dir3/file3.txt", index)
  1172. def test_add_directory_with_tracked_files(self) -> None:
  1173. """Test adding a directory with some files already tracked."""
  1174. # Create a subdirectory with files
  1175. subdir = os.path.join(self.repo.path, "mixed")
  1176. os.mkdir(subdir)
  1177. # Create and commit one file
  1178. tracked_file = os.path.join(subdir, "tracked.txt")
  1179. with open(tracked_file, "w") as f:
  1180. f.write("already tracked")
  1181. porcelain.add(self.repo.path, paths=[tracked_file])
  1182. porcelain.commit(
  1183. repo=self.repo.path,
  1184. message=b"Add tracked file",
  1185. author=b"test <email>",
  1186. committer=b"test <email>",
  1187. )
  1188. # Add more untracked files
  1189. with open(os.path.join(subdir, "untracked1.txt"), "w") as f:
  1190. f.write("new file 1")
  1191. with open(os.path.join(subdir, "untracked2.txt"), "w") as f:
  1192. f.write("new file 2")
  1193. # Add the directory
  1194. added, ignored = porcelain.add(self.repo.path, paths=["mixed"])
  1195. # Should only add the untracked files
  1196. self.assertEqual(len(added), 2)
  1197. # Normalize paths to use forward slashes for comparison
  1198. added_normalized = [path.replace(os.sep, "/") for path in added]
  1199. self.assertIn("mixed/untracked1.txt", added_normalized)
  1200. self.assertIn("mixed/untracked2.txt", added_normalized)
  1201. self.assertNotIn("mixed/tracked.txt", added)
  1202. # Verify the index contains all files
  1203. index = self.repo.open_index()
  1204. self.assertIn(b"mixed/tracked.txt", index)
  1205. self.assertIn(b"mixed/untracked1.txt", index)
  1206. self.assertIn(b"mixed/untracked2.txt", index)
  1207. def test_add_directory_with_gitignore(self) -> None:
  1208. """Test adding a directory respects .gitignore patterns."""
  1209. # Create .gitignore
  1210. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  1211. f.write("*.log\n*.tmp\nbuild/\n")
  1212. # Create directory with mixed files
  1213. testdir = os.path.join(self.repo.path, "testdir")
  1214. os.mkdir(testdir)
  1215. # Create various files
  1216. with open(os.path.join(testdir, "important.txt"), "w") as f:
  1217. f.write("keep this")
  1218. with open(os.path.join(testdir, "debug.log"), "w") as f:
  1219. f.write("ignore this")
  1220. with open(os.path.join(testdir, "temp.tmp"), "w") as f:
  1221. f.write("ignore this too")
  1222. with open(os.path.join(testdir, "readme.md"), "w") as f:
  1223. f.write("keep this too")
  1224. # Create a build directory that should be ignored
  1225. builddir = os.path.join(testdir, "build")
  1226. os.mkdir(builddir)
  1227. with open(os.path.join(builddir, "output.txt"), "w") as f:
  1228. f.write("ignore entire directory")
  1229. # Add the directory
  1230. added, ignored = porcelain.add(self.repo.path, paths=["testdir"])
  1231. # Should only add non-ignored files
  1232. # Normalize paths to use forward slashes for comparison
  1233. added_normalized = {path.replace(os.sep, "/") for path in added}
  1234. self.assertEqual(
  1235. added_normalized, {"testdir/important.txt", "testdir/readme.md"}
  1236. )
  1237. # Check ignored files
  1238. # Normalize paths to use forward slashes for comparison
  1239. ignored_normalized = {path.replace(os.sep, "/") for path in ignored}
  1240. self.assertIn("testdir/debug.log", ignored_normalized)
  1241. self.assertIn("testdir/temp.tmp", ignored_normalized)
  1242. self.assertIn("testdir/build/", ignored_normalized)
  1243. def test_add_multiple_directories(self) -> None:
  1244. """Test adding multiple directories in one call."""
  1245. # Create multiple directories
  1246. for dirname in ["dir1", "dir2", "dir3"]:
  1247. dirpath = os.path.join(self.repo.path, dirname)
  1248. os.mkdir(dirpath)
  1249. # Add files to each directory
  1250. for i in range(2):
  1251. with open(os.path.join(dirpath, f"file{i}.txt"), "w") as f:
  1252. f.write(f"content {dirname} {i}")
  1253. # Add all directories at once
  1254. added, ignored = porcelain.add(self.repo.path, paths=["dir1", "dir2", "dir3"])
  1255. # Should add all files from all directories
  1256. self.assertEqual(len(added), 6)
  1257. # Normalize paths to use forward slashes for comparison
  1258. added_normalized = [path.replace(os.sep, "/") for path in added]
  1259. for dirname in ["dir1", "dir2", "dir3"]:
  1260. for i in range(2):
  1261. self.assertIn(f"{dirname}/file{i}.txt", added_normalized)
  1262. # Verify all files are staged
  1263. index = self.repo.open_index()
  1264. self.assertEqual(len(index), 6)
  1265. def test_add_default_paths_includes_modified_files(self) -> None:
  1266. """Test that add() with no paths includes both untracked and modified files."""
  1267. # Create and commit initial file
  1268. initial_file = os.path.join(self.repo.path, "existing.txt")
  1269. with open(initial_file, "w") as f:
  1270. f.write("initial content\n")
  1271. porcelain.add(repo=self.repo.path, paths=[initial_file])
  1272. porcelain.commit(
  1273. repo=self.repo.path,
  1274. message=b"initial commit",
  1275. author=b"test <email>",
  1276. committer=b"test <email>",
  1277. )
  1278. # Modify the existing file (this creates an unstaged change)
  1279. with open(initial_file, "w") as f:
  1280. f.write("modified content\n")
  1281. # Create a new untracked file
  1282. new_file = os.path.join(self.repo.path, "new.txt")
  1283. with open(new_file, "w") as f:
  1284. f.write("new file content\n")
  1285. # Call add() with no paths - should stage both modified and untracked files
  1286. added_files, ignored_files = porcelain.add(repo=self.repo.path)
  1287. # Verify both files were added
  1288. self.assertIn("existing.txt", added_files)
  1289. self.assertIn("new.txt", added_files)
  1290. self.assertEqual(len(ignored_files), 0)
  1291. # Verify both files are now staged
  1292. index = self.repo.open_index()
  1293. self.assertIn(b"existing.txt", index)
  1294. self.assertIn(b"new.txt", index)
  1295. class RemoveTests(PorcelainTestCase):
  1296. def test_remove_file(self) -> None:
  1297. fullpath = os.path.join(self.repo.path, "foo")
  1298. with open(fullpath, "w") as f:
  1299. f.write("BAR")
  1300. porcelain.add(self.repo.path, paths=[fullpath])
  1301. porcelain.commit(
  1302. repo=self.repo,
  1303. message=b"test",
  1304. author=b"test <email>",
  1305. committer=b"test <email>",
  1306. )
  1307. self.assertTrue(os.path.exists(os.path.join(self.repo.path, "foo")))
  1308. cwd = os.getcwd()
  1309. try:
  1310. os.chdir(self.repo.path)
  1311. porcelain.remove(self.repo.path, paths=["foo"])
  1312. finally:
  1313. os.chdir(cwd)
  1314. self.assertFalse(os.path.exists(os.path.join(self.repo.path, "foo")))
  1315. def test_remove_file_staged(self) -> None:
  1316. fullpath = os.path.join(self.repo.path, "foo")
  1317. with open(fullpath, "w") as f:
  1318. f.write("BAR")
  1319. cwd = os.getcwd()
  1320. try:
  1321. os.chdir(self.repo.path)
  1322. porcelain.add(self.repo.path, paths=[fullpath])
  1323. self.assertRaises(Exception, porcelain.rm, self.repo.path, paths=["foo"])
  1324. finally:
  1325. os.chdir(cwd)
  1326. def test_remove_file_removed_on_disk(self) -> None:
  1327. fullpath = os.path.join(self.repo.path, "foo")
  1328. with open(fullpath, "w") as f:
  1329. f.write("BAR")
  1330. porcelain.add(self.repo.path, paths=[fullpath])
  1331. cwd = os.getcwd()
  1332. try:
  1333. os.chdir(self.repo.path)
  1334. os.remove(fullpath)
  1335. porcelain.remove(self.repo.path, paths=["foo"])
  1336. finally:
  1337. os.chdir(cwd)
  1338. self.assertFalse(os.path.exists(os.path.join(self.repo.path, "foo")))
  1339. def test_remove_from_different_directory(self) -> None:
  1340. # Create a subdirectory with a file
  1341. subdir = os.path.join(self.repo.path, "mydir")
  1342. os.makedirs(subdir)
  1343. fullpath = os.path.join(subdir, "myfile")
  1344. with open(fullpath, "w") as f:
  1345. f.write("BAR")
  1346. # Add and commit the file
  1347. porcelain.add(self.repo.path, paths=[fullpath])
  1348. porcelain.commit(
  1349. repo=self.repo,
  1350. message=b"test",
  1351. author=b"test <email>",
  1352. committer=b"test <email>",
  1353. )
  1354. # Change to a different directory
  1355. cwd = os.getcwd()
  1356. tempdir = tempfile.mkdtemp()
  1357. try:
  1358. os.chdir(tempdir)
  1359. # Remove the file using relative path from repository root
  1360. porcelain.remove(self.repo.path, paths=["mydir/myfile"])
  1361. finally:
  1362. os.chdir(cwd)
  1363. os.rmdir(tempdir)
  1364. # Verify file was removed
  1365. self.assertFalse(os.path.exists(fullpath))
  1366. def test_remove_with_absolute_path(self) -> None:
  1367. # Create a file
  1368. fullpath = os.path.join(self.repo.path, "foo")
  1369. with open(fullpath, "w") as f:
  1370. f.write("BAR")
  1371. # Add and commit the file
  1372. porcelain.add(self.repo.path, paths=[fullpath])
  1373. porcelain.commit(
  1374. repo=self.repo,
  1375. message=b"test",
  1376. author=b"test <email>",
  1377. committer=b"test <email>",
  1378. )
  1379. # Change to a different directory
  1380. cwd = os.getcwd()
  1381. tempdir = tempfile.mkdtemp()
  1382. try:
  1383. os.chdir(tempdir)
  1384. # Remove the file using absolute path
  1385. porcelain.remove(self.repo.path, paths=[fullpath])
  1386. finally:
  1387. os.chdir(cwd)
  1388. os.rmdir(tempdir)
  1389. # Verify file was removed
  1390. self.assertFalse(os.path.exists(fullpath))
  1391. class LogTests(PorcelainTestCase):
  1392. def test_simple(self) -> None:
  1393. c1, c2, c3 = build_commit_graph(
  1394. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1395. )
  1396. self.repo.refs[b"HEAD"] = c3.id
  1397. self.maxDiff = None
  1398. outstream = StringIO()
  1399. porcelain.log(self.repo.path, outstream=outstream)
  1400. self.assertEqual(
  1401. outstream.getvalue(),
  1402. """\
  1403. --------------------------------------------------
  1404. commit: 4a3b887baa9ecb2d054d2469b628aef84e2d74f0
  1405. merge: 7508036b1cfec5aa9cef0d5a7f04abcecfe09112
  1406. Author: Test Author <test@nodomain.com>
  1407. Committer: Test Committer <test@nodomain.com>
  1408. Date: Fri Jan 01 2010 00:00:00 +0000
  1409. Commit 3
  1410. --------------------------------------------------
  1411. commit: 7508036b1cfec5aa9cef0d5a7f04abcecfe09112
  1412. Author: Test Author <test@nodomain.com>
  1413. Committer: Test Committer <test@nodomain.com>
  1414. Date: Fri Jan 01 2010 00:00:00 +0000
  1415. Commit 2
  1416. --------------------------------------------------
  1417. commit: 11d3cf672a19366435c1983c7340b008ec6b8bf3
  1418. Author: Test Author <test@nodomain.com>
  1419. Committer: Test Committer <test@nodomain.com>
  1420. Date: Fri Jan 01 2010 00:00:00 +0000
  1421. Commit 1
  1422. """,
  1423. )
  1424. def test_max_entries(self) -> None:
  1425. c1, c2, c3 = build_commit_graph(
  1426. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1427. )
  1428. self.repo.refs[b"HEAD"] = c3.id
  1429. outstream = StringIO()
  1430. porcelain.log(self.repo.path, outstream=outstream, max_entries=1)
  1431. self.assertEqual(1, outstream.getvalue().count("-" * 50))
  1432. def test_no_revisions(self) -> None:
  1433. outstream = StringIO()
  1434. porcelain.log(self.repo.path, outstream=outstream)
  1435. self.assertEqual("", outstream.getvalue())
  1436. def test_empty_message(self) -> None:
  1437. c1 = make_commit(message="")
  1438. self.repo.object_store.add_object(c1)
  1439. self.repo.refs[b"HEAD"] = c1.id
  1440. outstream = StringIO()
  1441. porcelain.log(self.repo.path, outstream=outstream)
  1442. self.assertEqual(
  1443. outstream.getvalue(),
  1444. """\
  1445. --------------------------------------------------
  1446. commit: 4a7ad5552fad70647a81fb9a4a923ccefcca4b76
  1447. Author: Test Author <test@nodomain.com>
  1448. Committer: Test Committer <test@nodomain.com>
  1449. Date: Fri Jan 01 2010 00:00:00 +0000
  1450. """,
  1451. )
  1452. class ShowTests(PorcelainTestCase):
  1453. def test_nolist(self) -> None:
  1454. c1, c2, c3 = build_commit_graph(
  1455. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1456. )
  1457. self.repo.refs[b"HEAD"] = c3.id
  1458. outstream = StringIO()
  1459. porcelain.show(self.repo.path, objects=c3.id, outstream=outstream)
  1460. self.assertTrue(outstream.getvalue().startswith("-" * 50))
  1461. def test_simple(self) -> None:
  1462. c1, c2, c3 = build_commit_graph(
  1463. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1464. )
  1465. self.repo.refs[b"HEAD"] = c3.id
  1466. outstream = StringIO()
  1467. porcelain.show(self.repo.path, objects=[c3.id], outstream=outstream)
  1468. self.assertTrue(outstream.getvalue().startswith("-" * 50))
  1469. def test_blob(self) -> None:
  1470. b = Blob.from_string(b"The Foo\n")
  1471. self.repo.object_store.add_object(b)
  1472. outstream = StringIO()
  1473. porcelain.show(self.repo.path, objects=[b.id], outstream=outstream)
  1474. self.assertEqual(outstream.getvalue(), "The Foo\n")
  1475. def test_commit_no_parent(self) -> None:
  1476. a = Blob.from_string(b"The Foo\n")
  1477. ta = Tree()
  1478. ta.add(b"somename", 0o100644, a.id)
  1479. ca = make_commit(tree=ta.id)
  1480. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1481. outstream = StringIO()
  1482. porcelain.show(self.repo.path, objects=[ca.id], outstream=outstream)
  1483. self.assertMultiLineEqual(
  1484. outstream.getvalue(),
  1485. """\
  1486. --------------------------------------------------
  1487. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1488. Author: Test Author <test@nodomain.com>
  1489. Committer: Test Committer <test@nodomain.com>
  1490. Date: Fri Jan 01 2010 00:00:00 +0000
  1491. Test message.
  1492. diff --git a/somename b/somename
  1493. new file mode 100644
  1494. index 0000000..ea5c7bf
  1495. --- /dev/null
  1496. +++ b/somename
  1497. @@ -0,0 +1 @@
  1498. +The Foo
  1499. """,
  1500. )
  1501. def test_tag(self) -> None:
  1502. a = Blob.from_string(b"The Foo\n")
  1503. ta = Tree()
  1504. ta.add(b"somename", 0o100644, a.id)
  1505. ca = make_commit(tree=ta.id)
  1506. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1507. porcelain.tag_create(
  1508. self.repo.path,
  1509. b"tryme",
  1510. b"foo <foo@bar.com>",
  1511. b"bar",
  1512. annotated=True,
  1513. objectish=ca.id,
  1514. tag_time=1552854211,
  1515. tag_timezone=0,
  1516. )
  1517. outstream = StringIO()
  1518. porcelain.show(self.repo, objects=[b"refs/tags/tryme"], outstream=outstream)
  1519. self.maxDiff = None
  1520. self.assertMultiLineEqual(
  1521. outstream.getvalue(),
  1522. """\
  1523. Tagger: foo <foo@bar.com>
  1524. Date: Sun Mar 17 2019 20:23:31 +0000
  1525. bar
  1526. --------------------------------------------------
  1527. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1528. Author: Test Author <test@nodomain.com>
  1529. Committer: Test Committer <test@nodomain.com>
  1530. Date: Fri Jan 01 2010 00:00:00 +0000
  1531. Test message.
  1532. diff --git a/somename b/somename
  1533. new file mode 100644
  1534. index 0000000..ea5c7bf
  1535. --- /dev/null
  1536. +++ b/somename
  1537. @@ -0,0 +1 @@
  1538. +The Foo
  1539. """,
  1540. )
  1541. def test_tag_unicode(self) -> None:
  1542. a = Blob.from_string(b"The Foo\n")
  1543. ta = Tree()
  1544. ta.add(b"somename", 0o100644, a.id)
  1545. ca = make_commit(tree=ta.id)
  1546. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1547. porcelain.tag_create(
  1548. self.repo.path,
  1549. "tryme",
  1550. "foo <foo@bar.com>",
  1551. "bar",
  1552. annotated=True,
  1553. objectish=ca.id,
  1554. tag_time=1552854211,
  1555. tag_timezone=0,
  1556. )
  1557. outstream = StringIO()
  1558. porcelain.show(self.repo, objects=[b"refs/tags/tryme"], outstream=outstream)
  1559. self.maxDiff = None
  1560. self.assertMultiLineEqual(
  1561. outstream.getvalue(),
  1562. """\
  1563. Tagger: foo <foo@bar.com>
  1564. Date: Sun Mar 17 2019 20:23:31 +0000
  1565. bar
  1566. --------------------------------------------------
  1567. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1568. Author: Test Author <test@nodomain.com>
  1569. Committer: Test Committer <test@nodomain.com>
  1570. Date: Fri Jan 01 2010 00:00:00 +0000
  1571. Test message.
  1572. diff --git a/somename b/somename
  1573. new file mode 100644
  1574. index 0000000..ea5c7bf
  1575. --- /dev/null
  1576. +++ b/somename
  1577. @@ -0,0 +1 @@
  1578. +The Foo
  1579. """,
  1580. )
  1581. def test_commit_with_change(self) -> None:
  1582. a = Blob.from_string(b"The Foo\n")
  1583. ta = Tree()
  1584. ta.add(b"somename", 0o100644, a.id)
  1585. ca = make_commit(tree=ta.id)
  1586. b = Blob.from_string(b"The Bar\n")
  1587. tb = Tree()
  1588. tb.add(b"somename", 0o100644, b.id)
  1589. cb = make_commit(tree=tb.id, parents=[ca.id])
  1590. self.repo.object_store.add_objects(
  1591. [
  1592. (a, None),
  1593. (b, None),
  1594. (ta, None),
  1595. (tb, None),
  1596. (ca, None),
  1597. (cb, None),
  1598. ]
  1599. )
  1600. outstream = StringIO()
  1601. porcelain.show(self.repo.path, objects=[cb.id], outstream=outstream)
  1602. self.assertMultiLineEqual(
  1603. outstream.getvalue(),
  1604. """\
  1605. --------------------------------------------------
  1606. commit: 2c6b6c9cb72c130956657e1fdae58e5b103744fa
  1607. Author: Test Author <test@nodomain.com>
  1608. Committer: Test Committer <test@nodomain.com>
  1609. Date: Fri Jan 01 2010 00:00:00 +0000
  1610. Test message.
  1611. diff --git a/somename b/somename
  1612. index ea5c7bf..fd38bcb 100644
  1613. --- a/somename
  1614. +++ b/somename
  1615. @@ -1 +1 @@
  1616. -The Foo
  1617. +The Bar
  1618. """,
  1619. )
  1620. class SymbolicRefTests(PorcelainTestCase):
  1621. def test_set_wrong_symbolic_ref(self) -> None:
  1622. c1, c2, c3 = build_commit_graph(
  1623. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1624. )
  1625. self.repo.refs[b"HEAD"] = c3.id
  1626. self.assertRaises(
  1627. porcelain.Error, porcelain.symbolic_ref, self.repo.path, b"foobar"
  1628. )
  1629. def test_set_force_wrong_symbolic_ref(self) -> None:
  1630. c1, c2, c3 = build_commit_graph(
  1631. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1632. )
  1633. self.repo.refs[b"HEAD"] = c3.id
  1634. porcelain.symbolic_ref(self.repo.path, b"force_foobar", force=True)
  1635. # test if we actually changed the file
  1636. with self.repo.get_named_file("HEAD") as f:
  1637. new_ref = f.read()
  1638. self.assertEqual(new_ref, b"ref: refs/heads/force_foobar\n")
  1639. def test_set_symbolic_ref(self) -> None:
  1640. c1, c2, c3 = build_commit_graph(
  1641. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1642. )
  1643. self.repo.refs[b"HEAD"] = c3.id
  1644. porcelain.symbolic_ref(self.repo.path, b"master")
  1645. def test_set_symbolic_ref_other_than_master(self) -> None:
  1646. c1, c2, c3 = build_commit_graph(
  1647. self.repo.object_store,
  1648. [[1], [2, 1], [3, 1, 2]],
  1649. attrs=dict(refs="develop"),
  1650. )
  1651. self.repo.refs[b"HEAD"] = c3.id
  1652. self.repo.refs[b"refs/heads/develop"] = c3.id
  1653. porcelain.symbolic_ref(self.repo.path, b"develop")
  1654. # test if we actually changed the file
  1655. with self.repo.get_named_file("HEAD") as f:
  1656. new_ref = f.read()
  1657. self.assertEqual(new_ref, b"ref: refs/heads/develop\n")
  1658. class DiffTreeTests(PorcelainTestCase):
  1659. def test_empty(self) -> None:
  1660. c1, c2, c3 = build_commit_graph(
  1661. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1662. )
  1663. self.repo.refs[b"HEAD"] = c3.id
  1664. outstream = BytesIO()
  1665. porcelain.diff_tree(self.repo.path, c2.tree, c3.tree, outstream=outstream)
  1666. self.assertEqual(outstream.getvalue(), b"")
  1667. class CommitTreeTests(PorcelainTestCase):
  1668. def test_simple(self) -> None:
  1669. c1, c2, c3 = build_commit_graph(
  1670. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1671. )
  1672. b = Blob()
  1673. b.data = b"foo the bar"
  1674. t = Tree()
  1675. t.add(b"somename", 0o100644, b.id)
  1676. self.repo.object_store.add_object(t)
  1677. self.repo.object_store.add_object(b)
  1678. sha = porcelain.commit_tree(
  1679. self.repo.path,
  1680. t.id,
  1681. message=b"Withcommit.",
  1682. author=b"Joe <joe@example.com>",
  1683. committer=b"Jane <jane@example.com>",
  1684. )
  1685. self.assertIsInstance(sha, bytes)
  1686. self.assertEqual(len(sha), 40)
  1687. class RevListTests(PorcelainTestCase):
  1688. def test_simple(self) -> None:
  1689. c1, c2, c3 = build_commit_graph(
  1690. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1691. )
  1692. outstream = BytesIO()
  1693. porcelain.rev_list(self.repo.path, [c3.id], outstream=outstream)
  1694. self.assertEqual(
  1695. c3.id + b"\n" + c2.id + b"\n" + c1.id + b"\n", outstream.getvalue()
  1696. )
  1697. @skipIf(
  1698. platform.python_implementation() == "PyPy" or sys.platform == "win32",
  1699. "gpgme not easily available or supported on Windows and PyPy",
  1700. )
  1701. class TagCreateSignTests(PorcelainGpgTestCase):
  1702. def test_default_key(self) -> None:
  1703. c1, c2, c3 = build_commit_graph(
  1704. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1705. )
  1706. self.repo.refs[b"HEAD"] = c3.id
  1707. cfg = self.repo.get_config()
  1708. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  1709. self.import_default_key()
  1710. porcelain.tag_create(
  1711. self.repo.path,
  1712. b"tryme",
  1713. b"foo <foo@bar.com>",
  1714. b"bar",
  1715. annotated=True,
  1716. sign=True,
  1717. )
  1718. tags = self.repo.refs.as_dict(b"refs/tags")
  1719. self.assertEqual(list(tags.keys()), [b"tryme"])
  1720. tag = self.repo[b"refs/tags/tryme"]
  1721. self.assertIsInstance(tag, Tag)
  1722. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1723. self.assertEqual(b"bar\n", tag.message)
  1724. self.assertRecentTimestamp(tag.tag_time)
  1725. tag = self.repo[b"refs/tags/tryme"]
  1726. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  1727. tag.verify()
  1728. tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
  1729. self.import_non_default_key()
  1730. self.assertRaises(
  1731. gpg.errors.MissingSignatures,
  1732. tag.verify,
  1733. keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
  1734. )
  1735. tag._chunked_text = [b"bad data", tag._signature]
  1736. self.assertRaises(
  1737. gpg.errors.BadSignatures,
  1738. tag.verify,
  1739. )
  1740. def test_non_default_key(self) -> None:
  1741. c1, c2, c3 = build_commit_graph(
  1742. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1743. )
  1744. self.repo.refs[b"HEAD"] = c3.id
  1745. cfg = self.repo.get_config()
  1746. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  1747. self.import_non_default_key()
  1748. porcelain.tag_create(
  1749. self.repo.path,
  1750. b"tryme",
  1751. b"foo <foo@bar.com>",
  1752. b"bar",
  1753. annotated=True,
  1754. sign=PorcelainGpgTestCase.NON_DEFAULT_KEY_ID,
  1755. )
  1756. tags = self.repo.refs.as_dict(b"refs/tags")
  1757. self.assertEqual(list(tags.keys()), [b"tryme"])
  1758. tag = self.repo[b"refs/tags/tryme"]
  1759. self.assertIsInstance(tag, Tag)
  1760. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1761. self.assertEqual(b"bar\n", tag.message)
  1762. self.assertRecentTimestamp(tag.tag_time)
  1763. tag = self.repo[b"refs/tags/tryme"]
  1764. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  1765. tag.verify()
  1766. class TagCreateTests(PorcelainTestCase):
  1767. def test_annotated(self) -> None:
  1768. c1, c2, c3 = build_commit_graph(
  1769. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1770. )
  1771. self.repo.refs[b"HEAD"] = c3.id
  1772. porcelain.tag_create(
  1773. self.repo.path,
  1774. b"tryme",
  1775. b"foo <foo@bar.com>",
  1776. b"bar",
  1777. annotated=True,
  1778. )
  1779. tags = self.repo.refs.as_dict(b"refs/tags")
  1780. self.assertEqual(list(tags.keys()), [b"tryme"])
  1781. tag = self.repo[b"refs/tags/tryme"]
  1782. self.assertIsInstance(tag, Tag)
  1783. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1784. self.assertEqual(b"bar\n", tag.message)
  1785. self.assertRecentTimestamp(tag.tag_time)
  1786. def test_unannotated(self) -> None:
  1787. c1, c2, c3 = build_commit_graph(
  1788. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1789. )
  1790. self.repo.refs[b"HEAD"] = c3.id
  1791. porcelain.tag_create(self.repo.path, b"tryme", annotated=False)
  1792. tags = self.repo.refs.as_dict(b"refs/tags")
  1793. self.assertEqual(list(tags.keys()), [b"tryme"])
  1794. self.repo[b"refs/tags/tryme"]
  1795. self.assertEqual(list(tags.values()), [self.repo.head()])
  1796. def test_unannotated_unicode(self) -> None:
  1797. c1, c2, c3 = build_commit_graph(
  1798. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1799. )
  1800. self.repo.refs[b"HEAD"] = c3.id
  1801. porcelain.tag_create(self.repo.path, "tryme", annotated=False)
  1802. tags = self.repo.refs.as_dict(b"refs/tags")
  1803. self.assertEqual(list(tags.keys()), [b"tryme"])
  1804. self.repo[b"refs/tags/tryme"]
  1805. self.assertEqual(list(tags.values()), [self.repo.head()])
  1806. class TagListTests(PorcelainTestCase):
  1807. def test_empty(self) -> None:
  1808. tags = porcelain.tag_list(self.repo.path)
  1809. self.assertEqual([], tags)
  1810. def test_simple(self) -> None:
  1811. self.repo.refs[b"refs/tags/foo"] = b"aa" * 20
  1812. self.repo.refs[b"refs/tags/bar/bla"] = b"bb" * 20
  1813. tags = porcelain.tag_list(self.repo.path)
  1814. self.assertEqual([b"bar/bla", b"foo"], tags)
  1815. class TagDeleteTests(PorcelainTestCase):
  1816. def test_simple(self) -> None:
  1817. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  1818. self.repo[b"HEAD"] = c1.id
  1819. porcelain.tag_create(self.repo, b"foo")
  1820. self.assertIn(b"foo", porcelain.tag_list(self.repo))
  1821. porcelain.tag_delete(self.repo, b"foo")
  1822. self.assertNotIn(b"foo", porcelain.tag_list(self.repo))
  1823. class ResetTests(PorcelainTestCase):
  1824. def test_hard_head(self) -> None:
  1825. fullpath = os.path.join(self.repo.path, "foo")
  1826. with open(fullpath, "w") as f:
  1827. f.write("BAR")
  1828. porcelain.add(self.repo.path, paths=[fullpath])
  1829. porcelain.commit(
  1830. self.repo.path,
  1831. message=b"Some message",
  1832. committer=b"Jane <jane@example.com>",
  1833. author=b"John <john@example.com>",
  1834. )
  1835. with open(os.path.join(self.repo.path, "foo"), "wb") as f:
  1836. f.write(b"OOH")
  1837. porcelain.reset(self.repo, "hard", b"HEAD")
  1838. index = self.repo.open_index()
  1839. changes = list(
  1840. tree_changes(
  1841. self.repo,
  1842. index.commit(self.repo.object_store),
  1843. self.repo[b"HEAD"].tree,
  1844. )
  1845. )
  1846. self.assertEqual([], changes)
  1847. def test_hard_commit(self) -> None:
  1848. fullpath = os.path.join(self.repo.path, "foo")
  1849. with open(fullpath, "w") as f:
  1850. f.write("BAR")
  1851. porcelain.add(self.repo.path, paths=[fullpath])
  1852. sha = porcelain.commit(
  1853. self.repo.path,
  1854. message=b"Some message",
  1855. committer=b"Jane <jane@example.com>",
  1856. author=b"John <john@example.com>",
  1857. )
  1858. with open(fullpath, "wb") as f:
  1859. f.write(b"BAZ")
  1860. porcelain.add(self.repo.path, paths=[fullpath])
  1861. porcelain.commit(
  1862. self.repo.path,
  1863. message=b"Some other message",
  1864. committer=b"Jane <jane@example.com>",
  1865. author=b"John <john@example.com>",
  1866. )
  1867. porcelain.reset(self.repo, "hard", sha)
  1868. index = self.repo.open_index()
  1869. changes = list(
  1870. tree_changes(
  1871. self.repo,
  1872. index.commit(self.repo.object_store),
  1873. self.repo[sha].tree,
  1874. )
  1875. )
  1876. self.assertEqual([], changes)
  1877. def test_hard_commit_short_hash(self) -> None:
  1878. fullpath = os.path.join(self.repo.path, "foo")
  1879. with open(fullpath, "w") as f:
  1880. f.write("BAR")
  1881. porcelain.add(self.repo.path, paths=[fullpath])
  1882. sha = porcelain.commit(
  1883. self.repo.path,
  1884. message=b"Some message",
  1885. committer=b"Jane <jane@example.com>",
  1886. author=b"John <john@example.com>",
  1887. )
  1888. with open(fullpath, "wb") as f:
  1889. f.write(b"BAZ")
  1890. porcelain.add(self.repo.path, paths=[fullpath])
  1891. porcelain.commit(
  1892. self.repo.path,
  1893. message=b"Some other message",
  1894. committer=b"Jane <jane@example.com>",
  1895. author=b"John <john@example.com>",
  1896. )
  1897. # Test with short hash (7 characters)
  1898. short_sha = sha[:7].decode("ascii")
  1899. porcelain.reset(self.repo, "hard", short_sha)
  1900. index = self.repo.open_index()
  1901. changes = list(
  1902. tree_changes(
  1903. self.repo,
  1904. index.commit(self.repo.object_store),
  1905. self.repo[sha].tree,
  1906. )
  1907. )
  1908. self.assertEqual([], changes)
  1909. def test_hard_deletes_untracked_files(self) -> None:
  1910. """Test that reset --hard deletes files that don't exist in target tree."""
  1911. # Create and commit a file
  1912. fullpath = os.path.join(self.repo.path, "foo")
  1913. with open(fullpath, "w") as f:
  1914. f.write("BAR")
  1915. porcelain.add(self.repo.path, paths=[fullpath])
  1916. sha1 = porcelain.commit(
  1917. self.repo.path,
  1918. message=b"First commit",
  1919. committer=b"Jane <jane@example.com>",
  1920. author=b"John <john@example.com>",
  1921. )
  1922. # Create another file and commit
  1923. fullpath2 = os.path.join(self.repo.path, "bar")
  1924. with open(fullpath2, "w") as f:
  1925. f.write("BAZ")
  1926. porcelain.add(self.repo.path, paths=[fullpath2])
  1927. porcelain.commit(
  1928. self.repo.path,
  1929. message=b"Second commit",
  1930. committer=b"Jane <jane@example.com>",
  1931. author=b"John <john@example.com>",
  1932. )
  1933. # Reset hard to first commit - this should delete 'bar'
  1934. porcelain.reset(self.repo, "hard", sha1)
  1935. # Check that 'foo' still exists and 'bar' is deleted
  1936. self.assertTrue(os.path.exists(fullpath))
  1937. self.assertFalse(os.path.exists(fullpath2))
  1938. # Check index matches first commit
  1939. index = self.repo.open_index()
  1940. self.assertIn(b"foo", index)
  1941. self.assertNotIn(b"bar", index)
  1942. def test_hard_deletes_files_in_subdirs(self) -> None:
  1943. """Test that reset --hard deletes files in subdirectories."""
  1944. # Create and commit files in subdirectory
  1945. subdir = os.path.join(self.repo.path, "subdir")
  1946. os.makedirs(subdir)
  1947. file1 = os.path.join(subdir, "file1")
  1948. file2 = os.path.join(subdir, "file2")
  1949. with open(file1, "w") as f:
  1950. f.write("content1")
  1951. with open(file2, "w") as f:
  1952. f.write("content2")
  1953. porcelain.add(self.repo.path, paths=[file1, file2])
  1954. porcelain.commit(
  1955. self.repo.path,
  1956. message=b"First commit",
  1957. committer=b"Jane <jane@example.com>",
  1958. author=b"John <john@example.com>",
  1959. )
  1960. # Remove one file from subdirectory and commit
  1961. porcelain.rm(self.repo.path, paths=[file2])
  1962. sha2 = porcelain.commit(
  1963. self.repo.path,
  1964. message=b"Remove file2",
  1965. committer=b"Jane <jane@example.com>",
  1966. author=b"John <john@example.com>",
  1967. )
  1968. # Create file2 again (untracked)
  1969. with open(file2, "w") as f:
  1970. f.write("new content")
  1971. # Reset to commit that has file2 removed - should delete untracked file2
  1972. porcelain.reset(self.repo, "hard", sha2)
  1973. self.assertTrue(os.path.exists(file1))
  1974. self.assertFalse(os.path.exists(file2))
  1975. def test_hard_reset_to_remote_branch(self) -> None:
  1976. """Test reset --hard to remote branch deletes local files not in remote."""
  1977. # Create a file and commit
  1978. file1 = os.path.join(self.repo.path, "file1")
  1979. with open(file1, "w") as f:
  1980. f.write("content1")
  1981. porcelain.add(self.repo.path, paths=[file1])
  1982. sha1 = porcelain.commit(
  1983. self.repo.path,
  1984. message=b"Initial commit",
  1985. committer=b"Jane <jane@example.com>",
  1986. author=b"John <john@example.com>",
  1987. )
  1988. # Create a "remote" ref that doesn't have additional files
  1989. self.repo.refs[b"refs/remotes/origin/master"] = sha1
  1990. # Add another file locally and commit
  1991. file2 = os.path.join(self.repo.path, "file2")
  1992. with open(file2, "w") as f:
  1993. f.write("content2")
  1994. porcelain.add(self.repo.path, paths=[file2])
  1995. porcelain.commit(
  1996. self.repo.path,
  1997. message=b"Add file2",
  1998. committer=b"Jane <jane@example.com>",
  1999. author=b"John <john@example.com>",
  2000. )
  2001. # Both files should exist
  2002. self.assertTrue(os.path.exists(file1))
  2003. self.assertTrue(os.path.exists(file2))
  2004. # Reset to remote branch - should delete file2
  2005. porcelain.reset(self.repo, "hard", b"refs/remotes/origin/master")
  2006. # file1 should exist, file2 should be deleted
  2007. self.assertTrue(os.path.exists(file1))
  2008. self.assertFalse(os.path.exists(file2))
  2009. class ResetFileTests(PorcelainTestCase):
  2010. def test_reset_modify_file_to_commit(self) -> None:
  2011. file = "foo"
  2012. full_path = os.path.join(self.repo.path, file)
  2013. with open(full_path, "w") as f:
  2014. f.write("hello")
  2015. porcelain.add(self.repo, paths=[full_path])
  2016. sha = porcelain.commit(
  2017. self.repo,
  2018. message=b"unitest",
  2019. committer=b"Jane <jane@example.com>",
  2020. author=b"John <john@example.com>",
  2021. )
  2022. with open(full_path, "a") as f:
  2023. f.write("something new")
  2024. porcelain.reset_file(self.repo, file, target=sha)
  2025. with open(full_path) as f:
  2026. self.assertEqual("hello", f.read())
  2027. def test_reset_remove_file_to_commit(self) -> None:
  2028. file = "foo"
  2029. full_path = os.path.join(self.repo.path, file)
  2030. with open(full_path, "w") as f:
  2031. f.write("hello")
  2032. porcelain.add(self.repo, paths=[full_path])
  2033. sha = porcelain.commit(
  2034. self.repo,
  2035. message=b"unitest",
  2036. committer=b"Jane <jane@example.com>",
  2037. author=b"John <john@example.com>",
  2038. )
  2039. os.remove(full_path)
  2040. porcelain.reset_file(self.repo, file, target=sha)
  2041. with open(full_path) as f:
  2042. self.assertEqual("hello", f.read())
  2043. def test_resetfile_with_dir(self) -> None:
  2044. os.mkdir(os.path.join(self.repo.path, "new_dir"))
  2045. full_path = os.path.join(self.repo.path, "new_dir", "foo")
  2046. with open(full_path, "w") as f:
  2047. f.write("hello")
  2048. porcelain.add(self.repo, paths=[full_path])
  2049. sha = porcelain.commit(
  2050. self.repo,
  2051. message=b"unitest",
  2052. committer=b"Jane <jane@example.com>",
  2053. author=b"John <john@example.com>",
  2054. )
  2055. with open(full_path, "a") as f:
  2056. f.write("something new")
  2057. porcelain.commit(
  2058. self.repo,
  2059. message=b"unitest 2",
  2060. committer=b"Jane <jane@example.com>",
  2061. author=b"John <john@example.com>",
  2062. )
  2063. porcelain.reset_file(self.repo, os.path.join("new_dir", "foo"), target=sha)
  2064. with open(full_path) as f:
  2065. self.assertEqual("hello", f.read())
  2066. def _commit_file_with_content(repo, filename, content):
  2067. file_path = os.path.join(repo.path, filename)
  2068. with open(file_path, "w") as f:
  2069. f.write(content)
  2070. porcelain.add(repo, paths=[file_path])
  2071. sha = porcelain.commit(
  2072. repo,
  2073. message=b"add " + filename.encode(),
  2074. committer=b"Jane <jane@example.com>",
  2075. author=b"John <john@example.com>",
  2076. )
  2077. return sha, file_path
  2078. class RevertTests(PorcelainTestCase):
  2079. def test_revert_simple(self) -> None:
  2080. # Create initial commit
  2081. fullpath = os.path.join(self.repo.path, "foo")
  2082. with open(fullpath, "w") as f:
  2083. f.write("initial content\n")
  2084. porcelain.add(self.repo.path, paths=[fullpath])
  2085. porcelain.commit(
  2086. self.repo.path,
  2087. message=b"Initial commit",
  2088. committer=b"Jane <jane@example.com>",
  2089. author=b"John <john@example.com>",
  2090. )
  2091. # Make a change
  2092. with open(fullpath, "w") as f:
  2093. f.write("modified content\n")
  2094. porcelain.add(self.repo.path, paths=[fullpath])
  2095. change_sha = porcelain.commit(
  2096. self.repo.path,
  2097. message=b"Change content",
  2098. committer=b"Jane <jane@example.com>",
  2099. author=b"John <john@example.com>",
  2100. )
  2101. # Revert the change
  2102. revert_sha = porcelain.revert(self.repo.path, commits=[change_sha])
  2103. # Check the file content is back to initial
  2104. with open(fullpath) as f:
  2105. self.assertEqual("initial content\n", f.read())
  2106. # Check the revert commit message
  2107. revert_commit = self.repo[revert_sha]
  2108. self.assertIn(b'Revert "Change content"', revert_commit.message)
  2109. self.assertIn(change_sha[:7], revert_commit.message)
  2110. def test_revert_multiple(self) -> None:
  2111. # Create initial commit
  2112. fullpath = os.path.join(self.repo.path, "foo")
  2113. with open(fullpath, "w") as f:
  2114. f.write("line1\n")
  2115. porcelain.add(self.repo.path, paths=[fullpath])
  2116. porcelain.commit(
  2117. self.repo.path,
  2118. message=b"Initial commit",
  2119. committer=b"Jane <jane@example.com>",
  2120. author=b"John <john@example.com>",
  2121. )
  2122. # Add line2
  2123. with open(fullpath, "a") as f:
  2124. f.write("line2\n")
  2125. porcelain.add(self.repo.path, paths=[fullpath])
  2126. commit1 = porcelain.commit(
  2127. self.repo.path,
  2128. message=b"Add line2",
  2129. committer=b"Jane <jane@example.com>",
  2130. author=b"John <john@example.com>",
  2131. )
  2132. # Add line3
  2133. with open(fullpath, "a") as f:
  2134. f.write("line3\n")
  2135. porcelain.add(self.repo.path, paths=[fullpath])
  2136. commit2 = porcelain.commit(
  2137. self.repo.path,
  2138. message=b"Add line3",
  2139. committer=b"Jane <jane@example.com>",
  2140. author=b"John <john@example.com>",
  2141. )
  2142. # Revert both commits (in reverse order)
  2143. porcelain.revert(self.repo.path, commits=[commit2, commit1])
  2144. # Check file is back to initial state
  2145. with open(fullpath) as f:
  2146. self.assertEqual("line1\n", f.read())
  2147. def test_revert_no_commit(self) -> None:
  2148. # Create initial commit
  2149. fullpath = os.path.join(self.repo.path, "foo")
  2150. with open(fullpath, "w") as f:
  2151. f.write("initial\n")
  2152. porcelain.add(self.repo.path, paths=[fullpath])
  2153. porcelain.commit(
  2154. self.repo.path,
  2155. message=b"Initial",
  2156. committer=b"Jane <jane@example.com>",
  2157. author=b"John <john@example.com>",
  2158. )
  2159. # Make a change
  2160. with open(fullpath, "w") as f:
  2161. f.write("changed\n")
  2162. porcelain.add(self.repo.path, paths=[fullpath])
  2163. change_sha = porcelain.commit(
  2164. self.repo.path,
  2165. message=b"Change",
  2166. committer=b"Jane <jane@example.com>",
  2167. author=b"John <john@example.com>",
  2168. )
  2169. # Revert with no_commit
  2170. result = porcelain.revert(self.repo.path, commits=[change_sha], no_commit=True)
  2171. # Should return None
  2172. self.assertIsNone(result)
  2173. # File should be reverted
  2174. with open(fullpath) as f:
  2175. self.assertEqual("initial\n", f.read())
  2176. # HEAD should still point to the change commit
  2177. self.assertEqual(self.repo.refs[b"HEAD"], change_sha)
  2178. def test_revert_custom_message(self) -> None:
  2179. # Create commits
  2180. fullpath = os.path.join(self.repo.path, "foo")
  2181. with open(fullpath, "w") as f:
  2182. f.write("initial\n")
  2183. porcelain.add(self.repo.path, paths=[fullpath])
  2184. porcelain.commit(
  2185. self.repo.path,
  2186. message=b"Initial",
  2187. committer=b"Jane <jane@example.com>",
  2188. author=b"John <john@example.com>",
  2189. )
  2190. with open(fullpath, "w") as f:
  2191. f.write("changed\n")
  2192. porcelain.add(self.repo.path, paths=[fullpath])
  2193. change_sha = porcelain.commit(
  2194. self.repo.path,
  2195. message=b"Change",
  2196. committer=b"Jane <jane@example.com>",
  2197. author=b"John <john@example.com>",
  2198. )
  2199. # Revert with custom message
  2200. custom_msg = "Custom revert message"
  2201. revert_sha = porcelain.revert(
  2202. self.repo.path, commits=[change_sha], message=custom_msg
  2203. )
  2204. # Check the message
  2205. revert_commit = self.repo[revert_sha]
  2206. self.assertEqual(custom_msg.encode("utf-8"), revert_commit.message)
  2207. def test_revert_no_parent(self) -> None:
  2208. # Try to revert the initial commit (no parent)
  2209. fullpath = os.path.join(self.repo.path, "foo")
  2210. with open(fullpath, "w") as f:
  2211. f.write("content\n")
  2212. porcelain.add(self.repo.path, paths=[fullpath])
  2213. initial_sha = porcelain.commit(
  2214. self.repo.path,
  2215. message=b"Initial",
  2216. committer=b"Jane <jane@example.com>",
  2217. author=b"John <john@example.com>",
  2218. )
  2219. # Should raise an error
  2220. with self.assertRaises(porcelain.Error) as cm:
  2221. porcelain.revert(self.repo.path, commits=[initial_sha])
  2222. self.assertIn("no parents", str(cm.exception))
  2223. class CheckoutTests(PorcelainTestCase):
  2224. def setUp(self) -> None:
  2225. super().setUp()
  2226. self._sha, self._foo_path = _commit_file_with_content(
  2227. self.repo, "foo", "hello\n"
  2228. )
  2229. porcelain.branch_create(self.repo, "uni")
  2230. def test_checkout_to_existing_branch(self) -> None:
  2231. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2232. porcelain.checkout(self.repo, b"uni")
  2233. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2234. def test_checkout_to_non_existing_branch(self) -> None:
  2235. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2236. with self.assertRaises(KeyError):
  2237. porcelain.checkout(self.repo, b"bob")
  2238. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2239. def test_checkout_to_branch_with_modified_files(self) -> None:
  2240. with open(self._foo_path, "a") as f:
  2241. f.write("new message\n")
  2242. porcelain.add(self.repo, paths=[self._foo_path])
  2243. status = list(porcelain.status(self.repo))
  2244. self.assertEqual(
  2245. [{"add": [], "delete": [], "modify": [b"foo"]}, [], []], status
  2246. )
  2247. # The new checkout behavior prevents switching with staged changes
  2248. with self.assertRaises(porcelain.CheckoutError):
  2249. porcelain.checkout(self.repo, b"uni")
  2250. # Should still be on master
  2251. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2252. # Force checkout should work
  2253. porcelain.checkout(self.repo, b"uni", force=True)
  2254. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2255. def test_checkout_with_deleted_files(self) -> None:
  2256. porcelain.remove(self.repo.path, [os.path.join(self.repo.path, "foo")])
  2257. status = list(porcelain.status(self.repo))
  2258. self.assertEqual(
  2259. [{"add": [], "delete": [b"foo"], "modify": []}, [], []], status
  2260. )
  2261. # The new checkout behavior prevents switching with staged deletions
  2262. with self.assertRaises(porcelain.CheckoutError):
  2263. porcelain.checkout(self.repo, b"uni")
  2264. # Should still be on master
  2265. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2266. # Force checkout should work
  2267. porcelain.checkout(self.repo, b"uni", force=True)
  2268. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2269. def test_checkout_to_branch_with_added_files(self) -> None:
  2270. file_path = os.path.join(self.repo.path, "bar")
  2271. with open(file_path, "w") as f:
  2272. f.write("bar content\n")
  2273. porcelain.add(self.repo, paths=[file_path])
  2274. status = list(porcelain.status(self.repo))
  2275. self.assertEqual(
  2276. [{"add": [b"bar"], "delete": [], "modify": []}, [], []], status
  2277. )
  2278. # Both branches have file 'foo' checkout should be fine.
  2279. porcelain.checkout(self.repo, b"uni")
  2280. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2281. status = list(porcelain.status(self.repo))
  2282. self.assertEqual(
  2283. [{"add": [b"bar"], "delete": [], "modify": []}, [], []], status
  2284. )
  2285. def test_checkout_to_branch_with_modified_file_not_present(self) -> None:
  2286. # Commit a new file that the other branch doesn't have.
  2287. _, nee_path = _commit_file_with_content(self.repo, "nee", "Good content\n")
  2288. # Modify the file the other branch doesn't have.
  2289. with open(nee_path, "a") as f:
  2290. f.write("bar content\n")
  2291. porcelain.add(self.repo, paths=[nee_path])
  2292. status = list(porcelain.status(self.repo))
  2293. self.assertEqual(
  2294. [{"add": [], "delete": [], "modify": [b"nee"]}, [], []], status
  2295. )
  2296. # The new checkout behavior allows switching if the file doesn't exist in target branch
  2297. # (changes can be preserved)
  2298. porcelain.checkout(self.repo, b"uni")
  2299. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2300. # The staged changes are lost and the file is removed from working tree
  2301. # because it doesn't exist in the target branch
  2302. status = list(porcelain.status(self.repo))
  2303. # File 'nee' is gone completely
  2304. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2305. self.assertFalse(os.path.exists(nee_path))
  2306. def test_checkout_to_branch_with_modified_file_not_present_forced(self) -> None:
  2307. # Commit a new file that the other branch doesn't have.
  2308. _, nee_path = _commit_file_with_content(self.repo, "nee", "Good content\n")
  2309. # Modify the file the other branch doesn't have.
  2310. with open(nee_path, "a") as f:
  2311. f.write("bar content\n")
  2312. porcelain.add(self.repo, paths=[nee_path])
  2313. status = list(porcelain.status(self.repo))
  2314. self.assertEqual(
  2315. [{"add": [], "delete": [], "modify": [b"nee"]}, [], []], status
  2316. )
  2317. # 'uni' branch doesn't have 'nee' and it has been modified, but we force to reset the entire index.
  2318. porcelain.checkout(self.repo, b"uni", force=True)
  2319. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2320. status = list(porcelain.status(self.repo))
  2321. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2322. def test_checkout_to_branch_with_unstaged_files(self) -> None:
  2323. # Edit `foo`.
  2324. with open(self._foo_path, "a") as f:
  2325. f.write("new message")
  2326. status = list(porcelain.status(self.repo))
  2327. self.assertEqual(
  2328. [{"add": [], "delete": [], "modify": []}, [b"foo"], []], status
  2329. )
  2330. # The new checkout behavior prevents switching with unstaged changes
  2331. with self.assertRaises(porcelain.CheckoutError):
  2332. porcelain.checkout(self.repo, b"uni")
  2333. # Should still be on master
  2334. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2335. # Force checkout should work
  2336. porcelain.checkout(self.repo, b"uni", force=True)
  2337. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2338. def test_checkout_to_branch_with_untracked_files(self) -> None:
  2339. with open(os.path.join(self.repo.path, "neu"), "a") as f:
  2340. f.write("new message\n")
  2341. status = list(porcelain.status(self.repo))
  2342. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], ["neu"]], status)
  2343. porcelain.checkout(self.repo, b"uni")
  2344. status = list(porcelain.status(self.repo))
  2345. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], ["neu"]], status)
  2346. def test_checkout_to_branch_with_new_files(self) -> None:
  2347. porcelain.checkout(self.repo, b"uni")
  2348. sub_directory = os.path.join(self.repo.path, "sub1")
  2349. os.mkdir(sub_directory)
  2350. for index in range(5):
  2351. _commit_file_with_content(
  2352. self.repo, "new_file_" + str(index + 1), "Some content\n"
  2353. )
  2354. _commit_file_with_content(
  2355. self.repo,
  2356. os.path.join("sub1", "new_file_" + str(index + 10)),
  2357. "Good content\n",
  2358. )
  2359. status = list(porcelain.status(self.repo))
  2360. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2361. porcelain.checkout(self.repo, b"master")
  2362. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2363. status = list(porcelain.status(self.repo))
  2364. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2365. porcelain.checkout(self.repo, b"uni")
  2366. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  2367. status = list(porcelain.status(self.repo))
  2368. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2369. def test_checkout_to_branch_with_file_in_sub_directory(self) -> None:
  2370. sub_directory = os.path.join(self.repo.path, "sub1", "sub2")
  2371. os.makedirs(sub_directory)
  2372. sub_directory_file = os.path.join(sub_directory, "neu")
  2373. with open(sub_directory_file, "w") as f:
  2374. f.write("new message\n")
  2375. porcelain.add(self.repo, paths=[sub_directory_file])
  2376. porcelain.commit(
  2377. self.repo,
  2378. message=b"add " + sub_directory_file.encode(),
  2379. committer=b"Jane <jane@example.com>",
  2380. author=b"John <john@example.com>",
  2381. )
  2382. status = list(porcelain.status(self.repo))
  2383. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2384. self.assertTrue(os.path.isdir(sub_directory))
  2385. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  2386. porcelain.checkout(self.repo, b"uni")
  2387. status = list(porcelain.status(self.repo))
  2388. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2389. self.assertFalse(os.path.isdir(sub_directory))
  2390. self.assertFalse(os.path.isdir(os.path.dirname(sub_directory)))
  2391. porcelain.checkout(self.repo, b"master")
  2392. self.assertTrue(os.path.isdir(sub_directory))
  2393. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  2394. def test_checkout_to_branch_with_multiple_files_in_sub_directory(self) -> None:
  2395. sub_directory = os.path.join(self.repo.path, "sub1", "sub2")
  2396. os.makedirs(sub_directory)
  2397. sub_directory_file_1 = os.path.join(sub_directory, "neu")
  2398. with open(sub_directory_file_1, "w") as f:
  2399. f.write("new message\n")
  2400. sub_directory_file_2 = os.path.join(sub_directory, "gus")
  2401. with open(sub_directory_file_2, "w") as f:
  2402. f.write("alternative message\n")
  2403. porcelain.add(self.repo, paths=[sub_directory_file_1, sub_directory_file_2])
  2404. porcelain.commit(
  2405. self.repo,
  2406. message=b"add files neu and gus.",
  2407. committer=b"Jane <jane@example.com>",
  2408. author=b"John <john@example.com>",
  2409. )
  2410. status = list(porcelain.status(self.repo))
  2411. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2412. self.assertTrue(os.path.isdir(sub_directory))
  2413. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  2414. porcelain.checkout(self.repo, b"uni")
  2415. status = list(porcelain.status(self.repo))
  2416. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  2417. self.assertFalse(os.path.isdir(sub_directory))
  2418. self.assertFalse(os.path.isdir(os.path.dirname(sub_directory)))
  2419. def _commit_something_wrong(self):
  2420. with open(self._foo_path, "a") as f:
  2421. f.write("something wrong")
  2422. porcelain.add(self.repo, paths=[self._foo_path])
  2423. return porcelain.commit(
  2424. self.repo,
  2425. message=b"I may added something wrong",
  2426. committer=b"Jane <jane@example.com>",
  2427. author=b"John <john@example.com>",
  2428. )
  2429. def test_checkout_to_commit_sha(self) -> None:
  2430. self._commit_something_wrong()
  2431. porcelain.checkout(self.repo, self._sha)
  2432. self.assertEqual(self._sha, self.repo.head())
  2433. def test_checkout_to_head(self) -> None:
  2434. new_sha = self._commit_something_wrong()
  2435. porcelain.checkout(self.repo, b"HEAD")
  2436. self.assertEqual(new_sha, self.repo.head())
  2437. def _checkout_remote_branch(self):
  2438. errstream = BytesIO()
  2439. outstream = BytesIO()
  2440. porcelain.commit(
  2441. repo=self.repo.path,
  2442. message=b"init",
  2443. author=b"author <email>",
  2444. committer=b"committer <email>",
  2445. )
  2446. # Setup target repo cloned from temp test repo
  2447. clone_path = tempfile.mkdtemp()
  2448. self.addCleanup(shutil.rmtree, clone_path)
  2449. target_repo = porcelain.clone(
  2450. self.repo.path, target=clone_path, errstream=errstream
  2451. )
  2452. try:
  2453. self.assertEqual(target_repo[b"HEAD"], self.repo[b"HEAD"])
  2454. finally:
  2455. target_repo.close()
  2456. # create a second file to be pushed back to origin
  2457. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  2458. os.close(handle)
  2459. porcelain.add(repo=clone_path, paths=[fullpath])
  2460. porcelain.commit(
  2461. repo=clone_path,
  2462. message=b"push",
  2463. author=b"author <email>",
  2464. committer=b"committer <email>",
  2465. )
  2466. # Setup a non-checked out branch in the remote
  2467. refs_path = b"refs/heads/foo"
  2468. new_id = self.repo[b"HEAD"].id
  2469. self.assertNotEqual(new_id, ZERO_SHA)
  2470. self.repo.refs[refs_path] = new_id
  2471. # Push to the remote
  2472. porcelain.push(
  2473. clone_path,
  2474. "origin",
  2475. b"HEAD:" + refs_path,
  2476. outstream=outstream,
  2477. errstream=errstream,
  2478. )
  2479. self.assertEqual(
  2480. target_repo.refs[b"refs/remotes/origin/foo"],
  2481. target_repo.refs[b"HEAD"],
  2482. )
  2483. # The new checkout behavior treats origin/foo as a ref and creates detached HEAD
  2484. porcelain.checkout(target_repo, b"origin/foo")
  2485. original_id = target_repo[b"HEAD"].id
  2486. uni_id = target_repo[b"refs/remotes/origin/uni"].id
  2487. # Should be in detached HEAD state
  2488. with self.assertRaises((ValueError, IndexError)):
  2489. porcelain.active_branch(target_repo)
  2490. expected_refs = {
  2491. b"HEAD": original_id,
  2492. b"refs/heads/master": original_id,
  2493. # No local foo branch is created anymore
  2494. b"refs/remotes/origin/foo": original_id,
  2495. b"refs/remotes/origin/uni": uni_id,
  2496. b"refs/remotes/origin/HEAD": new_id,
  2497. b"refs/remotes/origin/master": new_id,
  2498. }
  2499. self.assertEqual(expected_refs, target_repo.get_refs())
  2500. return target_repo
  2501. def test_checkout_remote_branch(self) -> None:
  2502. repo = self._checkout_remote_branch()
  2503. repo.close()
  2504. def test_checkout_remote_branch_then_master_then_remote_branch_again(self) -> None:
  2505. target_repo = self._checkout_remote_branch()
  2506. # Should be in detached HEAD state
  2507. with self.assertRaises((ValueError, IndexError)):
  2508. porcelain.active_branch(target_repo)
  2509. # Save the commit SHA before adding bar
  2510. detached_commit_sha, _ = _commit_file_with_content(
  2511. target_repo, "bar", "something\n"
  2512. )
  2513. self.assertTrue(os.path.isfile(os.path.join(target_repo.path, "bar")))
  2514. porcelain.checkout(target_repo, b"master")
  2515. self.assertEqual(b"master", porcelain.active_branch(target_repo))
  2516. self.assertFalse(os.path.isfile(os.path.join(target_repo.path, "bar")))
  2517. # Going back to origin/foo won't have bar because the commit was made in detached state
  2518. porcelain.checkout(target_repo, b"origin/foo")
  2519. # Should be in detached HEAD state again
  2520. with self.assertRaises((ValueError, IndexError)):
  2521. porcelain.active_branch(target_repo)
  2522. # bar is NOT there because we're back at the original origin/foo commit
  2523. self.assertFalse(os.path.isfile(os.path.join(target_repo.path, "bar")))
  2524. # But we can checkout the specific commit to get bar back
  2525. porcelain.checkout(target_repo, detached_commit_sha.decode())
  2526. self.assertTrue(os.path.isfile(os.path.join(target_repo.path, "bar")))
  2527. target_repo.close()
  2528. class GeneralCheckoutTests(PorcelainTestCase):
  2529. """Tests for the general checkout function that handles branches, tags, and commits."""
  2530. def setUp(self) -> None:
  2531. super().setUp()
  2532. # Create initial commit
  2533. self._sha1, self._foo_path = _commit_file_with_content(
  2534. self.repo, "foo", "initial content\n"
  2535. )
  2536. # Create a branch
  2537. porcelain.branch_create(self.repo, "feature")
  2538. # Create another commit on master
  2539. self._sha2, self._bar_path = _commit_file_with_content(
  2540. self.repo, "bar", "bar content\n"
  2541. )
  2542. # Create a tag
  2543. porcelain.tag_create(self.repo, "v1.0", objectish=self._sha1)
  2544. def test_checkout_branch(self) -> None:
  2545. """Test checking out a branch."""
  2546. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2547. # Checkout feature branch
  2548. porcelain.checkout(self.repo, "feature")
  2549. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  2550. # File 'bar' should not exist in feature branch
  2551. self.assertFalse(os.path.exists(self._bar_path))
  2552. # Go back to master
  2553. porcelain.checkout(self.repo, "master")
  2554. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2555. # File 'bar' should exist again
  2556. self.assertTrue(os.path.exists(self._bar_path))
  2557. def test_checkout_commit(self) -> None:
  2558. """Test checking out a specific commit (detached HEAD)."""
  2559. # Checkout first commit by SHA
  2560. porcelain.checkout(self.repo, self._sha1.decode("ascii"))
  2561. # Should be in detached HEAD state - active_branch raises IndexError
  2562. with self.assertRaises((ValueError, IndexError)):
  2563. porcelain.active_branch(self.repo)
  2564. # File 'bar' should not exist
  2565. self.assertFalse(os.path.exists(self._bar_path))
  2566. # HEAD should point to the commit
  2567. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  2568. def test_checkout_tag(self) -> None:
  2569. """Test checking out a tag (detached HEAD)."""
  2570. # Checkout tag
  2571. porcelain.checkout(self.repo, "v1.0")
  2572. # Should be in detached HEAD state - active_branch raises IndexError
  2573. with self.assertRaises((ValueError, IndexError)):
  2574. porcelain.active_branch(self.repo)
  2575. # File 'bar' should not exist (tag points to first commit)
  2576. self.assertFalse(os.path.exists(self._bar_path))
  2577. # HEAD should point to the tagged commit
  2578. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  2579. def test_checkout_new_branch(self) -> None:
  2580. """Test creating a new branch during checkout (like git checkout -b)."""
  2581. # Create and checkout new branch from current HEAD
  2582. porcelain.checkout(self.repo, "master", new_branch="new-feature")
  2583. self.assertEqual(b"new-feature", porcelain.active_branch(self.repo))
  2584. self.assertTrue(os.path.exists(self._bar_path))
  2585. # Create and checkout new branch from specific commit
  2586. porcelain.checkout(self.repo, self._sha1.decode("ascii"), new_branch="from-old")
  2587. self.assertEqual(b"from-old", porcelain.active_branch(self.repo))
  2588. self.assertFalse(os.path.exists(self._bar_path))
  2589. def test_checkout_with_uncommitted_changes(self) -> None:
  2590. """Test checkout behavior with uncommitted changes."""
  2591. # Modify a file
  2592. with open(self._foo_path, "w") as f:
  2593. f.write("modified content\n")
  2594. # Should raise error when trying to checkout
  2595. with self.assertRaises(porcelain.CheckoutError) as cm:
  2596. porcelain.checkout(self.repo, "feature")
  2597. self.assertIn("local changes", str(cm.exception))
  2598. self.assertIn("foo", str(cm.exception))
  2599. # Should still be on master
  2600. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2601. def test_checkout_force(self) -> None:
  2602. """Test forced checkout discards local changes."""
  2603. # Modify a file
  2604. with open(self._foo_path, "w") as f:
  2605. f.write("modified content\n")
  2606. # Force checkout should succeed
  2607. porcelain.checkout(self.repo, "feature", force=True)
  2608. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  2609. # Local changes should be discarded
  2610. with open(self._foo_path) as f:
  2611. content = f.read()
  2612. self.assertEqual("initial content\n", content)
  2613. def test_checkout_nonexistent_ref(self) -> None:
  2614. """Test checkout of non-existent branch/commit."""
  2615. with self.assertRaises(KeyError):
  2616. porcelain.checkout(self.repo, "nonexistent")
  2617. def test_checkout_partial_sha(self) -> None:
  2618. """Test checkout with partial SHA."""
  2619. # Git typically allows checkout with partial SHA
  2620. partial_sha = self._sha1.decode("ascii")[:7]
  2621. porcelain.checkout(self.repo, partial_sha)
  2622. # Should be in detached HEAD state at the right commit
  2623. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  2624. def test_checkout_preserves_untracked_files(self) -> None:
  2625. """Test that checkout preserves untracked files."""
  2626. # Create an untracked file
  2627. untracked_path = os.path.join(self.repo.path, "untracked.txt")
  2628. with open(untracked_path, "w") as f:
  2629. f.write("untracked content\n")
  2630. # Checkout another branch
  2631. porcelain.checkout(self.repo, "feature")
  2632. # Untracked file should still exist
  2633. self.assertTrue(os.path.exists(untracked_path))
  2634. with open(untracked_path) as f:
  2635. content = f.read()
  2636. self.assertEqual("untracked content\n", content)
  2637. def test_checkout_full_ref_paths(self) -> None:
  2638. """Test checkout with full ref paths."""
  2639. # Test checkout with full branch ref path
  2640. porcelain.checkout(self.repo, "refs/heads/feature")
  2641. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  2642. # Test checkout with full tag ref path
  2643. porcelain.checkout(self.repo, "refs/tags/v1.0")
  2644. # Should be in detached HEAD state
  2645. with self.assertRaises((ValueError, IndexError)):
  2646. porcelain.active_branch(self.repo)
  2647. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  2648. def test_checkout_bytes_vs_string_target(self) -> None:
  2649. """Test that checkout works with both bytes and string targets."""
  2650. # Test with string target
  2651. porcelain.checkout(self.repo, "feature")
  2652. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  2653. # Test with bytes target
  2654. porcelain.checkout(self.repo, b"master")
  2655. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  2656. def test_checkout_new_branch_from_commit(self) -> None:
  2657. """Test creating a new branch from a specific commit."""
  2658. # Create new branch from first commit
  2659. porcelain.checkout(self.repo, self._sha1.decode(), new_branch="from-commit")
  2660. self.assertEqual(b"from-commit", porcelain.active_branch(self.repo))
  2661. # Should be at the first commit (no bar file)
  2662. self.assertFalse(os.path.exists(self._bar_path))
  2663. def test_checkout_with_staged_addition(self) -> None:
  2664. """Test checkout behavior with staged file additions."""
  2665. # Create and stage a new file that doesn't exist in target branch
  2666. new_file_path = os.path.join(self.repo.path, "new.txt")
  2667. with open(new_file_path, "w") as f:
  2668. f.write("new file content\n")
  2669. porcelain.add(self.repo, [new_file_path])
  2670. # This should succeed because the file doesn't exist in target branch
  2671. porcelain.checkout(self.repo, "feature")
  2672. # Should be on feature branch
  2673. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  2674. # The new file should still exist and be staged
  2675. self.assertTrue(os.path.exists(new_file_path))
  2676. status = porcelain.status(self.repo)
  2677. self.assertIn(b"new.txt", status.staged["add"])
  2678. def test_checkout_with_staged_modification_conflict(self) -> None:
  2679. """Test checkout behavior with staged modifications that would conflict."""
  2680. # Stage changes to a file that exists in both branches
  2681. with open(self._foo_path, "w") as f:
  2682. f.write("modified content\n")
  2683. porcelain.add(self.repo, [self._foo_path])
  2684. # Should prevent checkout due to staged changes to existing file
  2685. with self.assertRaises(porcelain.CheckoutError) as cm:
  2686. porcelain.checkout(self.repo, "feature")
  2687. self.assertIn("local changes", str(cm.exception))
  2688. self.assertIn("foo", str(cm.exception))
  2689. def test_checkout_head_reference(self) -> None:
  2690. """Test checkout of HEAD reference."""
  2691. # Move to feature branch first
  2692. porcelain.checkout(self.repo, "feature")
  2693. # Checkout HEAD creates detached HEAD state
  2694. porcelain.checkout(self.repo, "HEAD")
  2695. # Should be in detached HEAD state
  2696. with self.assertRaises((ValueError, IndexError)):
  2697. porcelain.active_branch(self.repo)
  2698. def test_checkout_error_messages(self) -> None:
  2699. """Test that checkout error messages are helpful."""
  2700. # Create uncommitted changes
  2701. with open(self._foo_path, "w") as f:
  2702. f.write("uncommitted changes\n")
  2703. # Try to checkout
  2704. with self.assertRaises(porcelain.CheckoutError) as cm:
  2705. porcelain.checkout(self.repo, "feature")
  2706. error_msg = str(cm.exception)
  2707. self.assertIn("local changes", error_msg)
  2708. self.assertIn("foo", error_msg)
  2709. self.assertIn("overwritten", error_msg)
  2710. self.assertIn("commit or stash", error_msg)
  2711. class SubmoduleTests(PorcelainTestCase):
  2712. def test_empty(self) -> None:
  2713. porcelain.commit(
  2714. repo=self.repo.path,
  2715. message=b"init",
  2716. author=b"author <email>",
  2717. committer=b"committer <email>",
  2718. )
  2719. self.assertEqual([], list(porcelain.submodule_list(self.repo)))
  2720. def test_add(self) -> None:
  2721. porcelain.submodule_add(self.repo, "../bar.git", "bar")
  2722. with open(f"{self.repo.path}/.gitmodules") as f:
  2723. self.assertEqual(
  2724. """\
  2725. [submodule "bar"]
  2726. \turl = ../bar.git
  2727. \tpath = bar
  2728. """,
  2729. f.read(),
  2730. )
  2731. def test_init(self) -> None:
  2732. porcelain.submodule_add(self.repo, "../bar.git", "bar")
  2733. porcelain.submodule_init(self.repo)
  2734. class PushTests(PorcelainTestCase):
  2735. def test_simple(self) -> None:
  2736. """Basic test of porcelain push where self.repo is the remote. First
  2737. clone the remote, commit a file to the clone, then push the changes
  2738. back to the remote.
  2739. """
  2740. outstream = BytesIO()
  2741. errstream = BytesIO()
  2742. porcelain.commit(
  2743. repo=self.repo.path,
  2744. message=b"init",
  2745. author=b"author <email>",
  2746. committer=b"committer <email>",
  2747. )
  2748. # Setup target repo cloned from temp test repo
  2749. clone_path = tempfile.mkdtemp()
  2750. self.addCleanup(shutil.rmtree, clone_path)
  2751. target_repo = porcelain.clone(
  2752. self.repo.path, target=clone_path, errstream=errstream
  2753. )
  2754. try:
  2755. self.assertEqual(target_repo[b"HEAD"], self.repo[b"HEAD"])
  2756. finally:
  2757. target_repo.close()
  2758. # create a second file to be pushed back to origin
  2759. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  2760. os.close(handle)
  2761. porcelain.add(repo=clone_path, paths=[fullpath])
  2762. porcelain.commit(
  2763. repo=clone_path,
  2764. message=b"push",
  2765. author=b"author <email>",
  2766. committer=b"committer <email>",
  2767. )
  2768. # Setup a non-checked out branch in the remote
  2769. refs_path = b"refs/heads/foo"
  2770. new_id = self.repo[b"HEAD"].id
  2771. self.assertNotEqual(new_id, ZERO_SHA)
  2772. self.repo.refs[refs_path] = new_id
  2773. # Push to the remote
  2774. porcelain.push(
  2775. clone_path,
  2776. "origin",
  2777. b"HEAD:" + refs_path,
  2778. outstream=outstream,
  2779. errstream=errstream,
  2780. )
  2781. self.assertEqual(
  2782. target_repo.refs[b"refs/remotes/origin/foo"],
  2783. target_repo.refs[b"HEAD"],
  2784. )
  2785. # Check that the target and source
  2786. with Repo(clone_path) as r_clone:
  2787. self.assertEqual(
  2788. {
  2789. b"HEAD": new_id,
  2790. b"refs/heads/foo": r_clone[b"HEAD"].id,
  2791. b"refs/heads/master": new_id,
  2792. },
  2793. self.repo.get_refs(),
  2794. )
  2795. self.assertEqual(r_clone[b"HEAD"].id, self.repo[refs_path].id)
  2796. # Get the change in the target repo corresponding to the add
  2797. # this will be in the foo branch.
  2798. change = next(
  2799. iter(
  2800. tree_changes(
  2801. self.repo,
  2802. self.repo[b"HEAD"].tree,
  2803. self.repo[b"refs/heads/foo"].tree,
  2804. )
  2805. )
  2806. )
  2807. self.assertEqual(
  2808. os.path.basename(fullpath), change.new.path.decode("ascii")
  2809. )
  2810. def test_local_missing(self) -> None:
  2811. """Pushing a new branch."""
  2812. outstream = BytesIO()
  2813. errstream = BytesIO()
  2814. # Setup target repo cloned from temp test repo
  2815. clone_path = tempfile.mkdtemp()
  2816. self.addCleanup(shutil.rmtree, clone_path)
  2817. target_repo = porcelain.init(clone_path)
  2818. target_repo.close()
  2819. self.assertRaises(
  2820. porcelain.Error,
  2821. porcelain.push,
  2822. self.repo,
  2823. clone_path,
  2824. b"HEAD:refs/heads/master",
  2825. outstream=outstream,
  2826. errstream=errstream,
  2827. )
  2828. def test_new(self) -> None:
  2829. """Pushing a new branch."""
  2830. outstream = BytesIO()
  2831. errstream = BytesIO()
  2832. # Setup target repo cloned from temp test repo
  2833. clone_path = tempfile.mkdtemp()
  2834. self.addCleanup(shutil.rmtree, clone_path)
  2835. target_repo = porcelain.init(clone_path)
  2836. target_repo.close()
  2837. # create a second file to be pushed back to origin
  2838. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  2839. os.close(handle)
  2840. porcelain.add(repo=clone_path, paths=[fullpath])
  2841. new_id = porcelain.commit(
  2842. repo=self.repo,
  2843. message=b"push",
  2844. author=b"author <email>",
  2845. committer=b"committer <email>",
  2846. )
  2847. # Push to the remote
  2848. porcelain.push(
  2849. self.repo,
  2850. clone_path,
  2851. b"HEAD:refs/heads/master",
  2852. outstream=outstream,
  2853. errstream=errstream,
  2854. )
  2855. with Repo(clone_path) as r_clone:
  2856. self.assertEqual(
  2857. {
  2858. b"HEAD": new_id,
  2859. b"refs/heads/master": new_id,
  2860. },
  2861. r_clone.get_refs(),
  2862. )
  2863. def test_delete(self) -> None:
  2864. """Basic test of porcelain push, removing a branch."""
  2865. outstream = BytesIO()
  2866. errstream = BytesIO()
  2867. porcelain.commit(
  2868. repo=self.repo.path,
  2869. message=b"init",
  2870. author=b"author <email>",
  2871. committer=b"committer <email>",
  2872. )
  2873. # Setup target repo cloned from temp test repo
  2874. clone_path = tempfile.mkdtemp()
  2875. self.addCleanup(shutil.rmtree, clone_path)
  2876. target_repo = porcelain.clone(
  2877. self.repo.path, target=clone_path, errstream=errstream
  2878. )
  2879. target_repo.close()
  2880. # Setup a non-checked out branch in the remote
  2881. refs_path = b"refs/heads/foo"
  2882. new_id = self.repo[b"HEAD"].id
  2883. self.assertNotEqual(new_id, ZERO_SHA)
  2884. self.repo.refs[refs_path] = new_id
  2885. # Push to the remote
  2886. porcelain.push(
  2887. clone_path,
  2888. self.repo.path,
  2889. b":" + refs_path,
  2890. outstream=outstream,
  2891. errstream=errstream,
  2892. )
  2893. self.assertEqual(
  2894. {
  2895. b"HEAD": new_id,
  2896. b"refs/heads/master": new_id,
  2897. },
  2898. self.repo.get_refs(),
  2899. )
  2900. def test_diverged(self) -> None:
  2901. outstream = BytesIO()
  2902. errstream = BytesIO()
  2903. porcelain.commit(
  2904. repo=self.repo.path,
  2905. message=b"init",
  2906. author=b"author <email>",
  2907. committer=b"committer <email>",
  2908. )
  2909. # Setup target repo cloned from temp test repo
  2910. clone_path = tempfile.mkdtemp()
  2911. self.addCleanup(shutil.rmtree, clone_path)
  2912. target_repo = porcelain.clone(
  2913. self.repo.path, target=clone_path, errstream=errstream
  2914. )
  2915. target_repo.close()
  2916. remote_id = porcelain.commit(
  2917. repo=self.repo.path,
  2918. message=b"remote change",
  2919. author=b"author <email>",
  2920. committer=b"committer <email>",
  2921. )
  2922. local_id = porcelain.commit(
  2923. repo=clone_path,
  2924. message=b"local change",
  2925. author=b"author <email>",
  2926. committer=b"committer <email>",
  2927. )
  2928. outstream = BytesIO()
  2929. errstream = BytesIO()
  2930. # Push to the remote
  2931. self.assertRaises(
  2932. porcelain.DivergedBranches,
  2933. porcelain.push,
  2934. clone_path,
  2935. self.repo.path,
  2936. b"refs/heads/master",
  2937. outstream=outstream,
  2938. errstream=errstream,
  2939. )
  2940. self.assertEqual(
  2941. {
  2942. b"HEAD": remote_id,
  2943. b"refs/heads/master": remote_id,
  2944. },
  2945. self.repo.get_refs(),
  2946. )
  2947. self.assertEqual(b"", outstream.getvalue())
  2948. self.assertEqual(b"", errstream.getvalue())
  2949. outstream = BytesIO()
  2950. errstream = BytesIO()
  2951. # Push to the remote with --force
  2952. porcelain.push(
  2953. clone_path,
  2954. self.repo.path,
  2955. b"refs/heads/master",
  2956. outstream=outstream,
  2957. errstream=errstream,
  2958. force=True,
  2959. )
  2960. self.assertEqual(
  2961. {
  2962. b"HEAD": local_id,
  2963. b"refs/heads/master": local_id,
  2964. },
  2965. self.repo.get_refs(),
  2966. )
  2967. self.assertEqual(b"", outstream.getvalue())
  2968. self.assertTrue(re.match(b"Push to .* successful.\n", errstream.getvalue()))
  2969. class PullTests(PorcelainTestCase):
  2970. def setUp(self) -> None:
  2971. super().setUp()
  2972. # create a file for initial commit
  2973. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2974. os.close(handle)
  2975. porcelain.add(repo=self.repo.path, paths=fullpath)
  2976. porcelain.commit(
  2977. repo=self.repo.path,
  2978. message=b"test",
  2979. author=b"test <email>",
  2980. committer=b"test <email>",
  2981. )
  2982. # Setup target repo
  2983. self.target_path = tempfile.mkdtemp()
  2984. self.addCleanup(shutil.rmtree, self.target_path)
  2985. target_repo = porcelain.clone(
  2986. self.repo.path, target=self.target_path, errstream=BytesIO()
  2987. )
  2988. target_repo.close()
  2989. # create a second file to be pushed
  2990. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2991. os.close(handle)
  2992. porcelain.add(repo=self.repo.path, paths=fullpath)
  2993. porcelain.commit(
  2994. repo=self.repo.path,
  2995. message=b"test2",
  2996. author=b"test2 <email>",
  2997. committer=b"test2 <email>",
  2998. )
  2999. self.assertIn(b"refs/heads/master", self.repo.refs)
  3000. self.assertIn(b"refs/heads/master", target_repo.refs)
  3001. def test_simple(self) -> None:
  3002. outstream = BytesIO()
  3003. errstream = BytesIO()
  3004. # Pull changes into the cloned repo
  3005. porcelain.pull(
  3006. self.target_path,
  3007. self.repo.path,
  3008. b"refs/heads/master",
  3009. outstream=outstream,
  3010. errstream=errstream,
  3011. )
  3012. # Check the target repo for pushed changes
  3013. with Repo(self.target_path) as r:
  3014. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  3015. def test_diverged(self) -> None:
  3016. outstream = BytesIO()
  3017. errstream = BytesIO()
  3018. c3a = porcelain.commit(
  3019. repo=self.target_path,
  3020. message=b"test3a",
  3021. author=b"test2 <email>",
  3022. committer=b"test2 <email>",
  3023. )
  3024. porcelain.commit(
  3025. repo=self.repo.path,
  3026. message=b"test3b",
  3027. author=b"test2 <email>",
  3028. committer=b"test2 <email>",
  3029. )
  3030. # Pull changes into the cloned repo
  3031. self.assertRaises(
  3032. porcelain.DivergedBranches,
  3033. porcelain.pull,
  3034. self.target_path,
  3035. self.repo.path,
  3036. b"refs/heads/master",
  3037. outstream=outstream,
  3038. errstream=errstream,
  3039. )
  3040. # Check the target repo for pushed changes
  3041. with Repo(self.target_path) as r:
  3042. self.assertEqual(r[b"refs/heads/master"].id, c3a)
  3043. # Pull with merge should now work
  3044. porcelain.pull(
  3045. self.target_path,
  3046. self.repo.path,
  3047. b"refs/heads/master",
  3048. outstream=outstream,
  3049. errstream=errstream,
  3050. fast_forward=False,
  3051. )
  3052. # Check the target repo for merged changes
  3053. with Repo(self.target_path) as r:
  3054. # HEAD should now be a merge commit
  3055. head = r[b"HEAD"]
  3056. # It should have two parents
  3057. self.assertEqual(len(head.parents), 2)
  3058. # One parent should be the previous HEAD (c3a)
  3059. self.assertIn(c3a, head.parents)
  3060. # The other parent should be from the source repo
  3061. self.assertIn(self.repo[b"HEAD"].id, head.parents)
  3062. def test_no_refspec(self) -> None:
  3063. outstream = BytesIO()
  3064. errstream = BytesIO()
  3065. # Pull changes into the cloned repo
  3066. porcelain.pull(
  3067. self.target_path,
  3068. self.repo.path,
  3069. outstream=outstream,
  3070. errstream=errstream,
  3071. )
  3072. # Check the target repo for pushed changes
  3073. with Repo(self.target_path) as r:
  3074. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  3075. def test_no_remote_location(self) -> None:
  3076. outstream = BytesIO()
  3077. errstream = BytesIO()
  3078. # Pull changes into the cloned repo
  3079. porcelain.pull(
  3080. self.target_path,
  3081. refspecs=b"refs/heads/master",
  3082. outstream=outstream,
  3083. errstream=errstream,
  3084. )
  3085. # Check the target repo for pushed changes
  3086. with Repo(self.target_path) as r:
  3087. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  3088. def test_pull_updates_working_tree(self) -> None:
  3089. """Test that pull updates the working tree with new files."""
  3090. outstream = BytesIO()
  3091. errstream = BytesIO()
  3092. # Create a new file with content in the source repo
  3093. new_file = os.path.join(self.repo.path, "newfile.txt")
  3094. with open(new_file, "w") as f:
  3095. f.write("This is new content")
  3096. porcelain.add(repo=self.repo.path, paths=[new_file])
  3097. porcelain.commit(
  3098. repo=self.repo.path,
  3099. message=b"Add new file",
  3100. author=b"test <email>",
  3101. committer=b"test <email>",
  3102. )
  3103. # Before pull, the file should not exist in target
  3104. target_file = os.path.join(self.target_path, "newfile.txt")
  3105. self.assertFalse(os.path.exists(target_file))
  3106. # Pull changes into the cloned repo
  3107. porcelain.pull(
  3108. self.target_path,
  3109. self.repo.path,
  3110. b"refs/heads/master",
  3111. outstream=outstream,
  3112. errstream=errstream,
  3113. )
  3114. # After pull, the file should exist with correct content
  3115. self.assertTrue(os.path.exists(target_file))
  3116. with open(target_file) as f:
  3117. self.assertEqual(f.read(), "This is new content")
  3118. # Check the HEAD is updated too
  3119. with Repo(self.target_path) as r:
  3120. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  3121. class StatusTests(PorcelainTestCase):
  3122. def test_empty(self) -> None:
  3123. results = porcelain.status(self.repo)
  3124. self.assertEqual({"add": [], "delete": [], "modify": []}, results.staged)
  3125. self.assertEqual([], results.unstaged)
  3126. def test_status_base(self) -> None:
  3127. """Integration test for `status` functionality."""
  3128. # Commit a dummy file then modify it
  3129. fullpath = os.path.join(self.repo.path, "foo")
  3130. with open(fullpath, "w") as f:
  3131. f.write("origstuff")
  3132. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3133. porcelain.commit(
  3134. repo=self.repo.path,
  3135. message=b"test status",
  3136. author=b"author <email>",
  3137. committer=b"committer <email>",
  3138. )
  3139. # modify access and modify time of path
  3140. os.utime(fullpath, (0, 0))
  3141. with open(fullpath, "wb") as f:
  3142. f.write(b"stuff")
  3143. # Make a dummy file and stage it
  3144. filename_add = "bar"
  3145. fullpath = os.path.join(self.repo.path, filename_add)
  3146. with open(fullpath, "w") as f:
  3147. f.write("stuff")
  3148. porcelain.add(repo=self.repo.path, paths=fullpath)
  3149. results = porcelain.status(self.repo)
  3150. self.assertEqual(results.staged["add"][0], filename_add.encode("ascii"))
  3151. self.assertEqual(results.unstaged, [b"foo"])
  3152. def test_status_all(self) -> None:
  3153. del_path = os.path.join(self.repo.path, "foo")
  3154. mod_path = os.path.join(self.repo.path, "bar")
  3155. add_path = os.path.join(self.repo.path, "baz")
  3156. us_path = os.path.join(self.repo.path, "blye")
  3157. ut_path = os.path.join(self.repo.path, "blyat")
  3158. with open(del_path, "w") as f:
  3159. f.write("origstuff")
  3160. with open(mod_path, "w") as f:
  3161. f.write("origstuff")
  3162. with open(us_path, "w") as f:
  3163. f.write("origstuff")
  3164. porcelain.add(repo=self.repo.path, paths=[del_path, mod_path, us_path])
  3165. porcelain.commit(
  3166. repo=self.repo.path,
  3167. message=b"test status",
  3168. author=b"author <email>",
  3169. committer=b"committer <email>",
  3170. )
  3171. porcelain.remove(self.repo.path, [del_path])
  3172. with open(add_path, "w") as f:
  3173. f.write("origstuff")
  3174. with open(mod_path, "w") as f:
  3175. f.write("more_origstuff")
  3176. with open(us_path, "w") as f:
  3177. f.write("more_origstuff")
  3178. porcelain.add(repo=self.repo.path, paths=[add_path, mod_path])
  3179. with open(us_path, "w") as f:
  3180. f.write("\norigstuff")
  3181. with open(ut_path, "w") as f:
  3182. f.write("origstuff")
  3183. results = porcelain.status(self.repo.path)
  3184. self.assertDictEqual(
  3185. {"add": [b"baz"], "delete": [b"foo"], "modify": [b"bar"]},
  3186. results.staged,
  3187. )
  3188. self.assertListEqual(results.unstaged, [b"blye"])
  3189. results_no_untracked = porcelain.status(self.repo.path, untracked_files="no")
  3190. self.assertListEqual(results_no_untracked.untracked, [])
  3191. def test_status_wrong_untracked_files_value(self) -> None:
  3192. with self.assertRaises(ValueError):
  3193. porcelain.status(self.repo.path, untracked_files="antani")
  3194. def test_status_untracked_path(self) -> None:
  3195. untracked_dir = os.path.join(self.repo_path, "untracked_dir")
  3196. os.mkdir(untracked_dir)
  3197. untracked_file = os.path.join(untracked_dir, "untracked_file")
  3198. with open(untracked_file, "w") as fh:
  3199. fh.write("untracked")
  3200. _, _, untracked = porcelain.status(self.repo.path, untracked_files="all")
  3201. self.assertEqual(untracked, ["untracked_dir/untracked_file"])
  3202. def test_status_untracked_path_normal(self) -> None:
  3203. # Create an untracked directory with multiple files
  3204. untracked_dir = os.path.join(self.repo_path, "untracked_dir")
  3205. os.mkdir(untracked_dir)
  3206. untracked_file1 = os.path.join(untracked_dir, "file1")
  3207. untracked_file2 = os.path.join(untracked_dir, "file2")
  3208. with open(untracked_file1, "w") as fh:
  3209. fh.write("untracked1")
  3210. with open(untracked_file2, "w") as fh:
  3211. fh.write("untracked2")
  3212. # Create a nested untracked directory
  3213. nested_dir = os.path.join(untracked_dir, "nested")
  3214. os.mkdir(nested_dir)
  3215. nested_file = os.path.join(nested_dir, "file3")
  3216. with open(nested_file, "w") as fh:
  3217. fh.write("untracked3")
  3218. # Test "normal" mode - should only show the directory, not individual files
  3219. _, _, untracked = porcelain.status(self.repo.path, untracked_files="normal")
  3220. self.assertEqual(untracked, ["untracked_dir/"])
  3221. # Test "all" mode - should show all files
  3222. _, _, untracked_all = porcelain.status(self.repo.path, untracked_files="all")
  3223. self.assertEqual(
  3224. sorted(untracked_all),
  3225. [
  3226. "untracked_dir/file1",
  3227. "untracked_dir/file2",
  3228. "untracked_dir/nested/file3",
  3229. ],
  3230. )
  3231. def test_status_mixed_tracked_untracked(self) -> None:
  3232. # Create a directory with both tracked and untracked files
  3233. mixed_dir = os.path.join(self.repo_path, "mixed_dir")
  3234. os.mkdir(mixed_dir)
  3235. # Add a tracked file
  3236. tracked_file = os.path.join(mixed_dir, "tracked.txt")
  3237. with open(tracked_file, "w") as fh:
  3238. fh.write("tracked content")
  3239. porcelain.add(self.repo.path, paths=[tracked_file])
  3240. porcelain.commit(
  3241. repo=self.repo.path,
  3242. message=b"add tracked file",
  3243. author=b"author <email>",
  3244. committer=b"committer <email>",
  3245. )
  3246. # Add untracked files to the same directory
  3247. untracked_file = os.path.join(mixed_dir, "untracked.txt")
  3248. with open(untracked_file, "w") as fh:
  3249. fh.write("untracked content")
  3250. # In "normal" mode, should show individual untracked files in mixed dirs
  3251. _, _, untracked = porcelain.status(self.repo.path, untracked_files="normal")
  3252. self.assertEqual(untracked, ["mixed_dir/untracked.txt"])
  3253. # In "all" mode, should be the same for mixed directories
  3254. _, _, untracked_all = porcelain.status(self.repo.path, untracked_files="all")
  3255. self.assertEqual(untracked_all, ["mixed_dir/untracked.txt"])
  3256. def test_status_crlf_mismatch(self) -> None:
  3257. # First make a commit as if the file has been added on a Linux system
  3258. # or with core.autocrlf=True
  3259. file_path = os.path.join(self.repo.path, "crlf")
  3260. with open(file_path, "wb") as f:
  3261. f.write(b"line1\nline2")
  3262. porcelain.add(repo=self.repo.path, paths=[file_path])
  3263. porcelain.commit(
  3264. repo=self.repo.path,
  3265. message=b"test status",
  3266. author=b"author <email>",
  3267. committer=b"committer <email>",
  3268. )
  3269. # Then update the file as if it was created by CGit on a Windows
  3270. # system with core.autocrlf=true
  3271. with open(file_path, "wb") as f:
  3272. f.write(b"line1\r\nline2")
  3273. results = porcelain.status(self.repo)
  3274. self.assertDictEqual({"add": [], "delete": [], "modify": []}, results.staged)
  3275. self.assertListEqual(results.unstaged, [b"crlf"])
  3276. self.assertListEqual(results.untracked, [])
  3277. def test_status_autocrlf_true(self) -> None:
  3278. # First make a commit as if the file has been added on a Linux system
  3279. # or with core.autocrlf=True
  3280. file_path = os.path.join(self.repo.path, "crlf")
  3281. with open(file_path, "wb") as f:
  3282. f.write(b"line1\nline2")
  3283. porcelain.add(repo=self.repo.path, paths=[file_path])
  3284. porcelain.commit(
  3285. repo=self.repo.path,
  3286. message=b"test status",
  3287. author=b"author <email>",
  3288. committer=b"committer <email>",
  3289. )
  3290. # Then update the file as if it was created by CGit on a Windows
  3291. # system with core.autocrlf=true
  3292. with open(file_path, "wb") as f:
  3293. f.write(b"line1\r\nline2")
  3294. # TODO: It should be set automatically by looking at the configuration
  3295. c = self.repo.get_config()
  3296. c.set("core", "autocrlf", True)
  3297. c.write_to_path()
  3298. results = porcelain.status(self.repo)
  3299. self.assertDictEqual({"add": [], "delete": [], "modify": []}, results.staged)
  3300. self.assertListEqual(results.unstaged, [])
  3301. self.assertListEqual(results.untracked, [])
  3302. def test_status_autocrlf_input(self) -> None:
  3303. # Commit existing file with CRLF
  3304. file_path = os.path.join(self.repo.path, "crlf-exists")
  3305. with open(file_path, "wb") as f:
  3306. f.write(b"line1\r\nline2")
  3307. porcelain.add(repo=self.repo.path, paths=[file_path])
  3308. porcelain.commit(
  3309. repo=self.repo.path,
  3310. message=b"test status",
  3311. author=b"author <email>",
  3312. committer=b"committer <email>",
  3313. )
  3314. c = self.repo.get_config()
  3315. c.set("core", "autocrlf", "input")
  3316. c.write_to_path()
  3317. # Add new (untracked) file
  3318. file_path = os.path.join(self.repo.path, "crlf-new")
  3319. with open(file_path, "wb") as f:
  3320. f.write(b"line1\r\nline2")
  3321. porcelain.add(repo=self.repo.path, paths=[file_path])
  3322. results = porcelain.status(self.repo)
  3323. self.assertDictEqual(
  3324. {"add": [b"crlf-new"], "delete": [], "modify": []}, results.staged
  3325. )
  3326. self.assertListEqual(results.unstaged, [])
  3327. self.assertListEqual(results.untracked, [])
  3328. def test_get_tree_changes_add(self) -> None:
  3329. """Unit test for get_tree_changes add."""
  3330. # Make a dummy file, stage
  3331. filename = "bar"
  3332. fullpath = os.path.join(self.repo.path, filename)
  3333. with open(fullpath, "w") as f:
  3334. f.write("stuff")
  3335. porcelain.add(repo=self.repo.path, paths=fullpath)
  3336. porcelain.commit(
  3337. repo=self.repo.path,
  3338. message=b"test status",
  3339. author=b"author <email>",
  3340. committer=b"committer <email>",
  3341. )
  3342. filename = "foo"
  3343. fullpath = os.path.join(self.repo.path, filename)
  3344. with open(fullpath, "w") as f:
  3345. f.write("stuff")
  3346. porcelain.add(repo=self.repo.path, paths=fullpath)
  3347. changes = porcelain.get_tree_changes(self.repo.path)
  3348. self.assertEqual(changes["add"][0], filename.encode("ascii"))
  3349. self.assertEqual(len(changes["add"]), 1)
  3350. self.assertEqual(len(changes["modify"]), 0)
  3351. self.assertEqual(len(changes["delete"]), 0)
  3352. def test_get_tree_changes_modify(self) -> None:
  3353. """Unit test for get_tree_changes modify."""
  3354. # Make a dummy file, stage, commit, modify
  3355. filename = "foo"
  3356. fullpath = os.path.join(self.repo.path, filename)
  3357. with open(fullpath, "w") as f:
  3358. f.write("stuff")
  3359. porcelain.add(repo=self.repo.path, paths=fullpath)
  3360. porcelain.commit(
  3361. repo=self.repo.path,
  3362. message=b"test status",
  3363. author=b"author <email>",
  3364. committer=b"committer <email>",
  3365. )
  3366. with open(fullpath, "w") as f:
  3367. f.write("otherstuff")
  3368. porcelain.add(repo=self.repo.path, paths=fullpath)
  3369. changes = porcelain.get_tree_changes(self.repo.path)
  3370. self.assertEqual(changes["modify"][0], filename.encode("ascii"))
  3371. self.assertEqual(len(changes["add"]), 0)
  3372. self.assertEqual(len(changes["modify"]), 1)
  3373. self.assertEqual(len(changes["delete"]), 0)
  3374. def test_get_tree_changes_delete(self) -> None:
  3375. """Unit test for get_tree_changes delete."""
  3376. # Make a dummy file, stage, commit, remove
  3377. filename = "foo"
  3378. fullpath = os.path.join(self.repo.path, filename)
  3379. with open(fullpath, "w") as f:
  3380. f.write("stuff")
  3381. porcelain.add(repo=self.repo.path, paths=fullpath)
  3382. porcelain.commit(
  3383. repo=self.repo.path,
  3384. message=b"test status",
  3385. author=b"author <email>",
  3386. committer=b"committer <email>",
  3387. )
  3388. cwd = os.getcwd()
  3389. try:
  3390. os.chdir(self.repo.path)
  3391. porcelain.remove(repo=self.repo.path, paths=[filename])
  3392. finally:
  3393. os.chdir(cwd)
  3394. changes = porcelain.get_tree_changes(self.repo.path)
  3395. self.assertEqual(changes["delete"][0], filename.encode("ascii"))
  3396. self.assertEqual(len(changes["add"]), 0)
  3397. self.assertEqual(len(changes["modify"]), 0)
  3398. self.assertEqual(len(changes["delete"]), 1)
  3399. def test_get_untracked_paths(self) -> None:
  3400. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3401. f.write("ignored\n")
  3402. with open(os.path.join(self.repo.path, "ignored"), "w") as f:
  3403. f.write("blah\n")
  3404. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  3405. f.write("blah\n")
  3406. os.symlink(
  3407. os.path.join(self.repo.path, os.pardir, "external_target"),
  3408. os.path.join(self.repo.path, "link"),
  3409. )
  3410. self.assertEqual(
  3411. {"ignored", "notignored", ".gitignore", "link"},
  3412. set(
  3413. porcelain.get_untracked_paths(
  3414. self.repo.path, self.repo.path, self.repo.open_index()
  3415. )
  3416. ),
  3417. )
  3418. self.assertEqual(
  3419. {".gitignore", "notignored", "link"},
  3420. set(porcelain.status(self.repo).untracked),
  3421. )
  3422. self.assertEqual(
  3423. {".gitignore", "notignored", "ignored", "link"},
  3424. set(porcelain.status(self.repo, ignored=True).untracked),
  3425. )
  3426. def test_get_untracked_paths_subrepo(self) -> None:
  3427. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3428. f.write("nested/\n")
  3429. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  3430. f.write("blah\n")
  3431. subrepo = Repo.init(os.path.join(self.repo.path, "nested"), mkdir=True)
  3432. with open(os.path.join(subrepo.path, "ignored"), "w") as f:
  3433. f.write("bleep\n")
  3434. with open(os.path.join(subrepo.path, "with"), "w") as f:
  3435. f.write("bloop\n")
  3436. with open(os.path.join(subrepo.path, "manager"), "w") as f:
  3437. f.write("blop\n")
  3438. self.assertEqual(
  3439. {".gitignore", "notignored", os.path.join("nested", "")},
  3440. set(
  3441. porcelain.get_untracked_paths(
  3442. self.repo.path, self.repo.path, self.repo.open_index()
  3443. )
  3444. ),
  3445. )
  3446. self.assertEqual(
  3447. {".gitignore", "notignored"},
  3448. set(
  3449. porcelain.get_untracked_paths(
  3450. self.repo.path,
  3451. self.repo.path,
  3452. self.repo.open_index(),
  3453. exclude_ignored=True,
  3454. )
  3455. ),
  3456. )
  3457. self.assertEqual(
  3458. {"ignored", "with", "manager"},
  3459. set(
  3460. porcelain.get_untracked_paths(
  3461. subrepo.path, subrepo.path, subrepo.open_index()
  3462. )
  3463. ),
  3464. )
  3465. self.assertEqual(
  3466. set(),
  3467. set(
  3468. porcelain.get_untracked_paths(
  3469. subrepo.path,
  3470. self.repo.path,
  3471. self.repo.open_index(),
  3472. )
  3473. ),
  3474. )
  3475. self.assertEqual(
  3476. {
  3477. os.path.join("nested", "ignored"),
  3478. os.path.join("nested", "with"),
  3479. os.path.join("nested", "manager"),
  3480. },
  3481. set(
  3482. porcelain.get_untracked_paths(
  3483. self.repo.path,
  3484. subrepo.path,
  3485. self.repo.open_index(),
  3486. )
  3487. ),
  3488. )
  3489. def test_get_untracked_paths_subdir(self) -> None:
  3490. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3491. f.write("subdir/\nignored")
  3492. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  3493. f.write("blah\n")
  3494. os.mkdir(os.path.join(self.repo.path, "subdir"))
  3495. with open(os.path.join(self.repo.path, "ignored"), "w") as f:
  3496. f.write("foo")
  3497. with open(os.path.join(self.repo.path, "subdir", "ignored"), "w") as f:
  3498. f.write("foo")
  3499. self.assertEqual(
  3500. {
  3501. ".gitignore",
  3502. "notignored",
  3503. "ignored",
  3504. os.path.join("subdir", ""),
  3505. },
  3506. set(
  3507. porcelain.get_untracked_paths(
  3508. self.repo.path,
  3509. self.repo.path,
  3510. self.repo.open_index(),
  3511. )
  3512. ),
  3513. )
  3514. self.assertEqual(
  3515. {".gitignore", "notignored"},
  3516. set(
  3517. porcelain.get_untracked_paths(
  3518. self.repo.path,
  3519. self.repo.path,
  3520. self.repo.open_index(),
  3521. exclude_ignored=True,
  3522. )
  3523. ),
  3524. )
  3525. def test_get_untracked_paths_invalid_untracked_files(self) -> None:
  3526. with self.assertRaises(ValueError):
  3527. list(
  3528. porcelain.get_untracked_paths(
  3529. self.repo.path,
  3530. self.repo.path,
  3531. self.repo.open_index(),
  3532. untracked_files="invalid_value",
  3533. )
  3534. )
  3535. def test_get_untracked_paths_normal(self) -> None:
  3536. # Create an untracked directory with files
  3537. untracked_dir = os.path.join(self.repo.path, "untracked_dir")
  3538. os.mkdir(untracked_dir)
  3539. with open(os.path.join(untracked_dir, "file1.txt"), "w") as f:
  3540. f.write("untracked content")
  3541. with open(os.path.join(untracked_dir, "file2.txt"), "w") as f:
  3542. f.write("more untracked content")
  3543. # Test that "normal" mode works and returns only the directory
  3544. _, _, untracked = porcelain.status(
  3545. repo=self.repo.path, untracked_files="normal"
  3546. )
  3547. self.assertEqual(untracked, ["untracked_dir/"])
  3548. def test_get_untracked_paths_top_level_issue_1247(self) -> None:
  3549. """Test for issue #1247: ensure top-level untracked files are detected."""
  3550. # Create a single top-level untracked file
  3551. with open(os.path.join(self.repo.path, "sample.txt"), "w") as f:
  3552. f.write("test content")
  3553. # Test get_untracked_paths directly
  3554. untracked = list(
  3555. porcelain.get_untracked_paths(
  3556. self.repo.path, self.repo.path, self.repo.open_index()
  3557. )
  3558. )
  3559. self.assertIn(
  3560. "sample.txt",
  3561. untracked,
  3562. "Top-level file 'sample.txt' should be in untracked list",
  3563. )
  3564. # Test via status
  3565. status = porcelain.status(self.repo)
  3566. self.assertIn(
  3567. "sample.txt",
  3568. status.untracked,
  3569. "Top-level file 'sample.txt' should be in status.untracked",
  3570. )
  3571. # TODO(jelmer): Add test for dulwich.porcelain.daemon
  3572. class UploadPackTests(PorcelainTestCase):
  3573. """Tests for upload_pack."""
  3574. def test_upload_pack(self) -> None:
  3575. outf = BytesIO()
  3576. exitcode = porcelain.upload_pack(self.repo.path, BytesIO(b"0000"), outf)
  3577. outlines = outf.getvalue().splitlines()
  3578. self.assertEqual([b"0000"], outlines)
  3579. self.assertEqual(0, exitcode)
  3580. class ReceivePackTests(PorcelainTestCase):
  3581. """Tests for receive_pack."""
  3582. def test_receive_pack(self) -> None:
  3583. filename = "foo"
  3584. fullpath = os.path.join(self.repo.path, filename)
  3585. with open(fullpath, "w") as f:
  3586. f.write("stuff")
  3587. porcelain.add(repo=self.repo.path, paths=fullpath)
  3588. self.repo.do_commit(
  3589. message=b"test status",
  3590. author=b"author <email>",
  3591. committer=b"committer <email>",
  3592. author_timestamp=1402354300,
  3593. commit_timestamp=1402354300,
  3594. author_timezone=0,
  3595. commit_timezone=0,
  3596. )
  3597. outf = BytesIO()
  3598. exitcode = porcelain.receive_pack(self.repo.path, BytesIO(b"0000"), outf)
  3599. outlines = outf.getvalue().splitlines()
  3600. self.assertEqual(
  3601. [
  3602. b"0091319b56ce3aee2d489f759736a79cc552c9bb86d9 HEAD\x00 report-status "
  3603. b"delete-refs quiet ofs-delta side-band-64k "
  3604. b"no-done symref=HEAD:refs/heads/master",
  3605. b"003f319b56ce3aee2d489f759736a79cc552c9bb86d9 refs/heads/master",
  3606. b"0000",
  3607. ],
  3608. outlines,
  3609. )
  3610. self.assertEqual(0, exitcode)
  3611. class BranchListTests(PorcelainTestCase):
  3612. def test_standard(self) -> None:
  3613. self.assertEqual(set(), set(porcelain.branch_list(self.repo)))
  3614. def test_new_branch(self) -> None:
  3615. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3616. self.repo[b"HEAD"] = c1.id
  3617. porcelain.branch_create(self.repo, b"foo")
  3618. self.assertEqual({b"master", b"foo"}, set(porcelain.branch_list(self.repo)))
  3619. class BranchCreateTests(PorcelainTestCase):
  3620. def test_branch_exists(self) -> None:
  3621. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3622. self.repo[b"HEAD"] = c1.id
  3623. porcelain.branch_create(self.repo, b"foo")
  3624. self.assertRaises(porcelain.Error, porcelain.branch_create, self.repo, b"foo")
  3625. porcelain.branch_create(self.repo, b"foo", force=True)
  3626. def test_new_branch(self) -> None:
  3627. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3628. self.repo[b"HEAD"] = c1.id
  3629. porcelain.branch_create(self.repo, b"foo")
  3630. self.assertEqual({b"master", b"foo"}, set(porcelain.branch_list(self.repo)))
  3631. class BranchDeleteTests(PorcelainTestCase):
  3632. def test_simple(self) -> None:
  3633. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3634. self.repo[b"HEAD"] = c1.id
  3635. porcelain.branch_create(self.repo, b"foo")
  3636. self.assertIn(b"foo", porcelain.branch_list(self.repo))
  3637. porcelain.branch_delete(self.repo, b"foo")
  3638. self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
  3639. def test_simple_unicode(self) -> None:
  3640. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3641. self.repo[b"HEAD"] = c1.id
  3642. porcelain.branch_create(self.repo, "foo")
  3643. self.assertIn(b"foo", porcelain.branch_list(self.repo))
  3644. porcelain.branch_delete(self.repo, "foo")
  3645. self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
  3646. class FetchTests(PorcelainTestCase):
  3647. def test_simple(self) -> None:
  3648. outstream = BytesIO()
  3649. errstream = BytesIO()
  3650. # create a file for initial commit
  3651. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  3652. os.close(handle)
  3653. porcelain.add(repo=self.repo.path, paths=fullpath)
  3654. porcelain.commit(
  3655. repo=self.repo.path,
  3656. message=b"test",
  3657. author=b"test <email>",
  3658. committer=b"test <email>",
  3659. )
  3660. # Setup target repo
  3661. target_path = tempfile.mkdtemp()
  3662. self.addCleanup(shutil.rmtree, target_path)
  3663. target_repo = porcelain.clone(
  3664. self.repo.path, target=target_path, errstream=errstream
  3665. )
  3666. # create a second file to be pushed
  3667. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  3668. os.close(handle)
  3669. porcelain.add(repo=self.repo.path, paths=fullpath)
  3670. porcelain.commit(
  3671. repo=self.repo.path,
  3672. message=b"test2",
  3673. author=b"test2 <email>",
  3674. committer=b"test2 <email>",
  3675. )
  3676. self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
  3677. target_repo.close()
  3678. # Fetch changes into the cloned repo
  3679. porcelain.fetch(target_path, "origin", outstream=outstream, errstream=errstream)
  3680. # Assert that fetch updated the local image of the remote
  3681. self.assert_correct_remote_refs(target_repo.get_refs(), self.repo.get_refs())
  3682. # Check the target repo for pushed changes
  3683. with Repo(target_path) as r:
  3684. self.assertIn(self.repo[b"HEAD"].id, r)
  3685. def test_with_remote_name(self) -> None:
  3686. remote_name = "origin"
  3687. outstream = BytesIO()
  3688. errstream = BytesIO()
  3689. # create a file for initial commit
  3690. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  3691. os.close(handle)
  3692. porcelain.add(repo=self.repo.path, paths=fullpath)
  3693. porcelain.commit(
  3694. repo=self.repo.path,
  3695. message=b"test",
  3696. author=b"test <email>",
  3697. committer=b"test <email>",
  3698. )
  3699. # Setup target repo
  3700. target_path = tempfile.mkdtemp()
  3701. self.addCleanup(shutil.rmtree, target_path)
  3702. target_repo = porcelain.clone(
  3703. self.repo.path, target=target_path, errstream=errstream
  3704. )
  3705. # Capture current refs
  3706. target_refs = target_repo.get_refs()
  3707. # create a second file to be pushed
  3708. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  3709. os.close(handle)
  3710. porcelain.add(repo=self.repo.path, paths=fullpath)
  3711. porcelain.commit(
  3712. repo=self.repo.path,
  3713. message=b"test2",
  3714. author=b"test2 <email>",
  3715. committer=b"test2 <email>",
  3716. )
  3717. self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
  3718. target_config = target_repo.get_config()
  3719. target_config.set(
  3720. (b"remote", remote_name.encode()), b"url", self.repo.path.encode()
  3721. )
  3722. target_repo.close()
  3723. # Fetch changes into the cloned repo
  3724. porcelain.fetch(
  3725. target_path, remote_name, outstream=outstream, errstream=errstream
  3726. )
  3727. # Assert that fetch updated the local image of the remote
  3728. self.assert_correct_remote_refs(target_repo.get_refs(), self.repo.get_refs())
  3729. # Check the target repo for pushed changes, as well as updates
  3730. # for the refs
  3731. with Repo(target_path) as r:
  3732. self.assertIn(self.repo[b"HEAD"].id, r)
  3733. self.assertNotEqual(self.repo.get_refs(), target_refs)
  3734. def assert_correct_remote_refs(
  3735. self, local_refs, remote_refs, remote_name=b"origin"
  3736. ) -> None:
  3737. """Assert that known remote refs corresponds to actual remote refs."""
  3738. local_ref_prefix = b"refs/heads"
  3739. remote_ref_prefix = b"refs/remotes/" + remote_name
  3740. locally_known_remote_refs = {
  3741. k[len(remote_ref_prefix) + 1 :]: v
  3742. for k, v in local_refs.items()
  3743. if k.startswith(remote_ref_prefix)
  3744. }
  3745. normalized_remote_refs = {
  3746. k[len(local_ref_prefix) + 1 :]: v
  3747. for k, v in remote_refs.items()
  3748. if k.startswith(local_ref_prefix)
  3749. }
  3750. if b"HEAD" in locally_known_remote_refs and b"HEAD" in remote_refs:
  3751. normalized_remote_refs[b"HEAD"] = remote_refs[b"HEAD"]
  3752. self.assertEqual(locally_known_remote_refs, normalized_remote_refs)
  3753. class RepackTests(PorcelainTestCase):
  3754. def test_empty(self) -> None:
  3755. porcelain.repack(self.repo)
  3756. def test_simple(self) -> None:
  3757. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  3758. os.close(handle)
  3759. porcelain.add(repo=self.repo.path, paths=fullpath)
  3760. porcelain.repack(self.repo)
  3761. class LsTreeTests(PorcelainTestCase):
  3762. def test_empty(self) -> None:
  3763. porcelain.commit(
  3764. repo=self.repo.path,
  3765. message=b"test status",
  3766. author=b"author <email>",
  3767. committer=b"committer <email>",
  3768. )
  3769. f = StringIO()
  3770. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  3771. self.assertEqual(f.getvalue(), "")
  3772. def test_simple(self) -> None:
  3773. # Commit a dummy file then modify it
  3774. fullpath = os.path.join(self.repo.path, "foo")
  3775. with open(fullpath, "w") as f:
  3776. f.write("origstuff")
  3777. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3778. porcelain.commit(
  3779. repo=self.repo.path,
  3780. message=b"test status",
  3781. author=b"author <email>",
  3782. committer=b"committer <email>",
  3783. )
  3784. f = StringIO()
  3785. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  3786. self.assertEqual(
  3787. f.getvalue(),
  3788. "100644 blob 8b82634d7eae019850bb883f06abf428c58bc9aa\tfoo\n",
  3789. )
  3790. def test_recursive(self) -> None:
  3791. # Create a directory then write a dummy file in it
  3792. dirpath = os.path.join(self.repo.path, "adir")
  3793. filepath = os.path.join(dirpath, "afile")
  3794. os.mkdir(dirpath)
  3795. with open(filepath, "w") as f:
  3796. f.write("origstuff")
  3797. porcelain.add(repo=self.repo.path, paths=[filepath])
  3798. porcelain.commit(
  3799. repo=self.repo.path,
  3800. message=b"test status",
  3801. author=b"author <email>",
  3802. committer=b"committer <email>",
  3803. )
  3804. f = StringIO()
  3805. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  3806. self.assertEqual(
  3807. f.getvalue(),
  3808. "40000 tree b145cc69a5e17693e24d8a7be0016ed8075de66d\tadir\n",
  3809. )
  3810. f = StringIO()
  3811. porcelain.ls_tree(self.repo, b"HEAD", outstream=f, recursive=True)
  3812. self.assertEqual(
  3813. f.getvalue(),
  3814. "40000 tree b145cc69a5e17693e24d8a7be0016ed8075de66d\tadir\n"
  3815. "100644 blob 8b82634d7eae019850bb883f06abf428c58bc9aa\tadir"
  3816. "/afile\n",
  3817. )
  3818. class LsRemoteTests(PorcelainTestCase):
  3819. def test_empty(self) -> None:
  3820. result = porcelain.ls_remote(self.repo.path)
  3821. self.assertEqual({}, result.refs)
  3822. self.assertEqual({}, result.symrefs)
  3823. def test_some(self) -> None:
  3824. cid = porcelain.commit(
  3825. repo=self.repo.path,
  3826. message=b"test status",
  3827. author=b"author <email>",
  3828. committer=b"committer <email>",
  3829. )
  3830. result = porcelain.ls_remote(self.repo.path)
  3831. self.assertEqual(
  3832. {b"refs/heads/master": cid, b"HEAD": cid},
  3833. result.refs,
  3834. )
  3835. # HEAD should be a symref to refs/heads/master
  3836. self.assertEqual({b"HEAD": b"refs/heads/master"}, result.symrefs)
  3837. class LsFilesTests(PorcelainTestCase):
  3838. def test_empty(self) -> None:
  3839. self.assertEqual([], list(porcelain.ls_files(self.repo)))
  3840. def test_simple(self) -> None:
  3841. # Commit a dummy file then modify it
  3842. fullpath = os.path.join(self.repo.path, "foo")
  3843. with open(fullpath, "w") as f:
  3844. f.write("origstuff")
  3845. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3846. self.assertEqual([b"foo"], list(porcelain.ls_files(self.repo)))
  3847. class RemoteAddTests(PorcelainTestCase):
  3848. def test_new(self) -> None:
  3849. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3850. c = self.repo.get_config()
  3851. self.assertEqual(
  3852. c.get((b"remote", b"jelmer"), b"url"),
  3853. b"git://jelmer.uk/code/dulwich",
  3854. )
  3855. def test_exists(self) -> None:
  3856. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3857. self.assertRaises(
  3858. porcelain.RemoteExists,
  3859. porcelain.remote_add,
  3860. self.repo,
  3861. "jelmer",
  3862. "git://jelmer.uk/code/dulwich",
  3863. )
  3864. class RemoteRemoveTests(PorcelainTestCase):
  3865. def test_remove(self) -> None:
  3866. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3867. c = self.repo.get_config()
  3868. self.assertEqual(
  3869. c.get((b"remote", b"jelmer"), b"url"),
  3870. b"git://jelmer.uk/code/dulwich",
  3871. )
  3872. porcelain.remote_remove(self.repo, "jelmer")
  3873. self.assertRaises(KeyError, porcelain.remote_remove, self.repo, "jelmer")
  3874. c = self.repo.get_config()
  3875. self.assertRaises(KeyError, c.get, (b"remote", b"jelmer"), b"url")
  3876. class CheckIgnoreTests(PorcelainTestCase):
  3877. def test_check_ignored(self) -> None:
  3878. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3879. f.write("foo")
  3880. foo_path = os.path.join(self.repo.path, "foo")
  3881. with open(foo_path, "w") as f:
  3882. f.write("BAR")
  3883. bar_path = os.path.join(self.repo.path, "bar")
  3884. with open(bar_path, "w") as f:
  3885. f.write("BAR")
  3886. self.assertEqual(["foo"], list(porcelain.check_ignore(self.repo, [foo_path])))
  3887. self.assertEqual([], list(porcelain.check_ignore(self.repo, [bar_path])))
  3888. def test_check_added_abs(self) -> None:
  3889. path = os.path.join(self.repo.path, "foo")
  3890. with open(path, "w") as f:
  3891. f.write("BAR")
  3892. self.repo.stage(["foo"])
  3893. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3894. f.write("foo\n")
  3895. self.assertEqual([], list(porcelain.check_ignore(self.repo, [path])))
  3896. self.assertEqual(
  3897. ["foo"],
  3898. list(porcelain.check_ignore(self.repo, [path], no_index=True)),
  3899. )
  3900. def test_check_added_rel(self) -> None:
  3901. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  3902. f.write("BAR")
  3903. self.repo.stage(["foo"])
  3904. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3905. f.write("foo\n")
  3906. cwd = os.getcwd()
  3907. os.mkdir(os.path.join(self.repo.path, "bar"))
  3908. os.chdir(os.path.join(self.repo.path, "bar"))
  3909. try:
  3910. self.assertEqual(list(porcelain.check_ignore(self.repo, ["../foo"])), [])
  3911. self.assertEqual(
  3912. ["../foo"],
  3913. list(porcelain.check_ignore(self.repo, ["../foo"], no_index=True)),
  3914. )
  3915. finally:
  3916. os.chdir(cwd)
  3917. class UpdateHeadTests(PorcelainTestCase):
  3918. def test_set_to_branch(self) -> None:
  3919. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3920. self.repo.refs[b"refs/heads/blah"] = c1.id
  3921. porcelain.update_head(self.repo, "blah")
  3922. self.assertEqual(c1.id, self.repo.head())
  3923. self.assertEqual(b"ref: refs/heads/blah", self.repo.refs.read_ref(b"HEAD"))
  3924. def test_set_to_branch_detached(self) -> None:
  3925. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3926. self.repo.refs[b"refs/heads/blah"] = c1.id
  3927. porcelain.update_head(self.repo, "blah", detached=True)
  3928. self.assertEqual(c1.id, self.repo.head())
  3929. self.assertEqual(c1.id, self.repo.refs.read_ref(b"HEAD"))
  3930. def test_set_to_commit_detached(self) -> None:
  3931. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3932. self.repo.refs[b"refs/heads/blah"] = c1.id
  3933. porcelain.update_head(self.repo, c1.id, detached=True)
  3934. self.assertEqual(c1.id, self.repo.head())
  3935. self.assertEqual(c1.id, self.repo.refs.read_ref(b"HEAD"))
  3936. def test_set_new_branch(self) -> None:
  3937. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3938. self.repo.refs[b"refs/heads/blah"] = c1.id
  3939. porcelain.update_head(self.repo, "blah", new_branch="bar")
  3940. self.assertEqual(c1.id, self.repo.head())
  3941. self.assertEqual(b"ref: refs/heads/bar", self.repo.refs.read_ref(b"HEAD"))
  3942. class MailmapTests(PorcelainTestCase):
  3943. def test_no_mailmap(self) -> None:
  3944. self.assertEqual(
  3945. b"Jelmer Vernooij <jelmer@samba.org>",
  3946. porcelain.check_mailmap(self.repo, b"Jelmer Vernooij <jelmer@samba.org>"),
  3947. )
  3948. def test_mailmap_lookup(self) -> None:
  3949. with open(os.path.join(self.repo.path, ".mailmap"), "wb") as f:
  3950. f.write(
  3951. b"""\
  3952. Jelmer Vernooij <jelmer@debian.org>
  3953. """
  3954. )
  3955. self.assertEqual(
  3956. b"Jelmer Vernooij <jelmer@debian.org>",
  3957. porcelain.check_mailmap(self.repo, b"Jelmer Vernooij <jelmer@samba.org>"),
  3958. )
  3959. class FsckTests(PorcelainTestCase):
  3960. def test_none(self) -> None:
  3961. self.assertEqual([], list(porcelain.fsck(self.repo)))
  3962. def test_git_dir(self) -> None:
  3963. obj = Tree()
  3964. a = Blob()
  3965. a.data = b"foo"
  3966. obj.add(b".git", 0o100644, a.id)
  3967. self.repo.object_store.add_objects([(a, None), (obj, None)])
  3968. self.assertEqual(
  3969. [(obj.id, "invalid name .git")],
  3970. [(sha, str(e)) for (sha, e) in porcelain.fsck(self.repo)],
  3971. )
  3972. class DescribeTests(PorcelainTestCase):
  3973. def test_no_commits(self) -> None:
  3974. self.assertRaises(KeyError, porcelain.describe, self.repo.path)
  3975. def test_single_commit(self) -> None:
  3976. fullpath = os.path.join(self.repo.path, "foo")
  3977. with open(fullpath, "w") as f:
  3978. f.write("BAR")
  3979. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3980. sha = porcelain.commit(
  3981. self.repo.path,
  3982. message=b"Some message",
  3983. author=b"Joe <joe@example.com>",
  3984. committer=b"Bob <bob@example.com>",
  3985. )
  3986. self.assertEqual(
  3987. "g{}".format(sha[:7].decode("ascii")),
  3988. porcelain.describe(self.repo.path),
  3989. )
  3990. def test_tag(self) -> None:
  3991. fullpath = os.path.join(self.repo.path, "foo")
  3992. with open(fullpath, "w") as f:
  3993. f.write("BAR")
  3994. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3995. porcelain.commit(
  3996. self.repo.path,
  3997. message=b"Some message",
  3998. author=b"Joe <joe@example.com>",
  3999. committer=b"Bob <bob@example.com>",
  4000. )
  4001. porcelain.tag_create(
  4002. self.repo.path,
  4003. b"tryme",
  4004. b"foo <foo@bar.com>",
  4005. b"bar",
  4006. annotated=True,
  4007. )
  4008. self.assertEqual("tryme", porcelain.describe(self.repo.path))
  4009. def test_tag_and_commit(self) -> None:
  4010. fullpath = os.path.join(self.repo.path, "foo")
  4011. with open(fullpath, "w") as f:
  4012. f.write("BAR")
  4013. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4014. porcelain.commit(
  4015. self.repo.path,
  4016. message=b"Some message",
  4017. author=b"Joe <joe@example.com>",
  4018. committer=b"Bob <bob@example.com>",
  4019. )
  4020. porcelain.tag_create(
  4021. self.repo.path,
  4022. b"tryme",
  4023. b"foo <foo@bar.com>",
  4024. b"bar",
  4025. annotated=True,
  4026. )
  4027. with open(fullpath, "w") as f:
  4028. f.write("BAR2")
  4029. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4030. sha = porcelain.commit(
  4031. self.repo.path,
  4032. message=b"Some message",
  4033. author=b"Joe <joe@example.com>",
  4034. committer=b"Bob <bob@example.com>",
  4035. )
  4036. self.assertEqual(
  4037. "tryme-1-g{}".format(sha[:7].decode("ascii")),
  4038. porcelain.describe(self.repo.path),
  4039. )
  4040. def test_tag_and_commit_full(self) -> None:
  4041. fullpath = os.path.join(self.repo.path, "foo")
  4042. with open(fullpath, "w") as f:
  4043. f.write("BAR")
  4044. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4045. porcelain.commit(
  4046. self.repo.path,
  4047. message=b"Some message",
  4048. author=b"Joe <joe@example.com>",
  4049. committer=b"Bob <bob@example.com>",
  4050. )
  4051. porcelain.tag_create(
  4052. self.repo.path,
  4053. b"tryme",
  4054. b"foo <foo@bar.com>",
  4055. b"bar",
  4056. annotated=True,
  4057. )
  4058. with open(fullpath, "w") as f:
  4059. f.write("BAR2")
  4060. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4061. sha = porcelain.commit(
  4062. self.repo.path,
  4063. message=b"Some message",
  4064. author=b"Joe <joe@example.com>",
  4065. committer=b"Bob <bob@example.com>",
  4066. )
  4067. self.assertEqual(
  4068. "tryme-1-g{}".format(sha.decode("ascii")),
  4069. porcelain.describe(self.repo.path, abbrev=40),
  4070. )
  4071. def test_untagged_commit_abbreviation(self) -> None:
  4072. _, _, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
  4073. self.repo.refs[b"HEAD"] = c3.id
  4074. brief_description, complete_description = (
  4075. porcelain.describe(self.repo),
  4076. porcelain.describe(self.repo, abbrev=40),
  4077. )
  4078. self.assertTrue(complete_description.startswith(brief_description))
  4079. self.assertEqual(
  4080. "g{}".format(c3.id.decode("ascii")),
  4081. complete_description,
  4082. )
  4083. class PathToTreeTests(PorcelainTestCase):
  4084. def setUp(self) -> None:
  4085. super().setUp()
  4086. self.fp = os.path.join(self.test_dir, "bar")
  4087. with open(self.fp, "w") as f:
  4088. f.write("something")
  4089. oldcwd = os.getcwd()
  4090. self.addCleanup(os.chdir, oldcwd)
  4091. os.chdir(self.test_dir)
  4092. def test_path_to_tree_path_base(self) -> None:
  4093. self.assertEqual(b"bar", porcelain.path_to_tree_path(self.test_dir, self.fp))
  4094. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "./bar"))
  4095. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "bar"))
  4096. cwd = os.getcwd()
  4097. self.assertEqual(
  4098. b"bar", porcelain.path_to_tree_path(".", os.path.join(cwd, "bar"))
  4099. )
  4100. self.assertEqual(b"bar", porcelain.path_to_tree_path(cwd, "bar"))
  4101. def test_path_to_tree_path_syntax(self) -> None:
  4102. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "./bar"))
  4103. def test_path_to_tree_path_error(self) -> None:
  4104. with self.assertRaises(ValueError):
  4105. with tempfile.TemporaryDirectory() as od:
  4106. porcelain.path_to_tree_path(od, self.fp)
  4107. def test_path_to_tree_path_rel(self) -> None:
  4108. cwd = os.getcwd()
  4109. os.mkdir(os.path.join(self.repo.path, "foo"))
  4110. os.mkdir(os.path.join(self.repo.path, "foo/bar"))
  4111. try:
  4112. os.chdir(os.path.join(self.repo.path, "foo/bar"))
  4113. with open("baz", "w") as f:
  4114. f.write("contents")
  4115. self.assertEqual(b"bar/baz", porcelain.path_to_tree_path("..", "baz"))
  4116. self.assertEqual(
  4117. b"bar/baz",
  4118. porcelain.path_to_tree_path(
  4119. os.path.join(os.getcwd(), ".."),
  4120. os.path.join(os.getcwd(), "baz"),
  4121. ),
  4122. )
  4123. self.assertEqual(
  4124. b"bar/baz",
  4125. porcelain.path_to_tree_path("..", os.path.join(os.getcwd(), "baz")),
  4126. )
  4127. self.assertEqual(
  4128. b"bar/baz",
  4129. porcelain.path_to_tree_path(os.path.join(os.getcwd(), ".."), "baz"),
  4130. )
  4131. finally:
  4132. os.chdir(cwd)
  4133. class GetObjectByPathTests(PorcelainTestCase):
  4134. def test_simple(self) -> None:
  4135. fullpath = os.path.join(self.repo.path, "foo")
  4136. with open(fullpath, "w") as f:
  4137. f.write("BAR")
  4138. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4139. porcelain.commit(
  4140. self.repo.path,
  4141. message=b"Some message",
  4142. author=b"Joe <joe@example.com>",
  4143. committer=b"Bob <bob@example.com>",
  4144. )
  4145. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, "foo").data)
  4146. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, b"foo").data)
  4147. def test_encoding(self) -> None:
  4148. fullpath = os.path.join(self.repo.path, "foo")
  4149. with open(fullpath, "w") as f:
  4150. f.write("BAR")
  4151. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4152. porcelain.commit(
  4153. self.repo.path,
  4154. message=b"Some message",
  4155. author=b"Joe <joe@example.com>",
  4156. committer=b"Bob <bob@example.com>",
  4157. encoding=b"utf-8",
  4158. )
  4159. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, "foo").data)
  4160. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, b"foo").data)
  4161. def test_missing(self) -> None:
  4162. self.assertRaises(KeyError, porcelain.get_object_by_path, self.repo, "foo")
  4163. class WriteTreeTests(PorcelainTestCase):
  4164. def test_simple(self) -> None:
  4165. fullpath = os.path.join(self.repo.path, "foo")
  4166. with open(fullpath, "w") as f:
  4167. f.write("BAR")
  4168. porcelain.add(repo=self.repo.path, paths=[fullpath])
  4169. self.assertEqual(
  4170. b"d2092c8a9f311f0311083bf8d177f2ca0ab5b241",
  4171. porcelain.write_tree(self.repo),
  4172. )
  4173. class ActiveBranchTests(PorcelainTestCase):
  4174. def test_simple(self) -> None:
  4175. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  4176. class FindUniqueAbbrevTests(PorcelainTestCase):
  4177. def test_simple(self) -> None:
  4178. c1, c2, c3 = build_commit_graph(
  4179. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  4180. )
  4181. self.repo.refs[b"HEAD"] = c3.id
  4182. self.assertEqual(
  4183. c1.id.decode("ascii")[:7],
  4184. porcelain.find_unique_abbrev(self.repo.object_store, c1.id),
  4185. )
  4186. class PackRefsTests(PorcelainTestCase):
  4187. def test_all(self) -> None:
  4188. c1, c2, c3 = build_commit_graph(
  4189. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  4190. )
  4191. self.repo.refs[b"HEAD"] = c3.id
  4192. self.repo.refs[b"refs/heads/master"] = c2.id
  4193. self.repo.refs[b"refs/tags/foo"] = c1.id
  4194. porcelain.pack_refs(self.repo, all=True)
  4195. self.assertEqual(
  4196. self.repo.refs.get_packed_refs(),
  4197. {
  4198. b"refs/heads/master": c2.id,
  4199. b"refs/tags/foo": c1.id,
  4200. },
  4201. )
  4202. def test_not_all(self) -> None:
  4203. c1, c2, c3 = build_commit_graph(
  4204. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  4205. )
  4206. self.repo.refs[b"HEAD"] = c3.id
  4207. self.repo.refs[b"refs/heads/master"] = c2.id
  4208. self.repo.refs[b"refs/tags/foo"] = c1.id
  4209. porcelain.pack_refs(self.repo)
  4210. self.assertEqual(
  4211. self.repo.refs.get_packed_refs(),
  4212. {
  4213. b"refs/tags/foo": c1.id,
  4214. },
  4215. )
  4216. class ServerTests(PorcelainTestCase):
  4217. @contextlib.contextmanager
  4218. def _serving(self):
  4219. with make_server("localhost", 0, self.app) as server:
  4220. thread = threading.Thread(target=server.serve_forever, daemon=True)
  4221. thread.start()
  4222. try:
  4223. yield f"http://localhost:{server.server_port}"
  4224. finally:
  4225. server.shutdown()
  4226. thread.join(10)
  4227. def setUp(self) -> None:
  4228. super().setUp()
  4229. self.served_repo_path = os.path.join(self.test_dir, "served_repo.git")
  4230. self.served_repo = Repo.init_bare(self.served_repo_path, mkdir=True)
  4231. self.addCleanup(self.served_repo.close)
  4232. backend = DictBackend({"/": self.served_repo})
  4233. self.app = make_wsgi_chain(backend)
  4234. def test_pull(self) -> None:
  4235. (c1,) = build_commit_graph(self.served_repo.object_store, [[1]])
  4236. self.served_repo.refs[b"refs/heads/master"] = c1.id
  4237. with self._serving() as url:
  4238. porcelain.pull(self.repo, url, "master")
  4239. def test_push(self) -> None:
  4240. (c1,) = build_commit_graph(self.repo.object_store, [[1]])
  4241. self.repo.refs[b"refs/heads/master"] = c1.id
  4242. with self._serving() as url:
  4243. porcelain.push(self.repo, url, "master")
  4244. class ForEachTests(PorcelainTestCase):
  4245. def setUp(self) -> None:
  4246. super().setUp()
  4247. c1, c2, c3, c4 = build_commit_graph(
  4248. self.repo.object_store, [[1], [2, 1], [3, 1, 2], [4]]
  4249. )
  4250. porcelain.tag_create(
  4251. self.repo.path,
  4252. b"v0.1",
  4253. objectish=c1.id,
  4254. annotated=True,
  4255. message=b"0.1",
  4256. )
  4257. porcelain.tag_create(
  4258. self.repo.path,
  4259. b"v1.0",
  4260. objectish=c2.id,
  4261. annotated=True,
  4262. message=b"1.0",
  4263. )
  4264. porcelain.tag_create(self.repo.path, b"simple-tag", objectish=c3.id)
  4265. porcelain.tag_create(
  4266. self.repo.path,
  4267. b"v1.1",
  4268. objectish=c4.id,
  4269. annotated=True,
  4270. message=b"1.1",
  4271. )
  4272. porcelain.branch_create(
  4273. self.repo.path, b"feat", objectish=c2.id.decode("ascii")
  4274. )
  4275. self.repo.refs[b"HEAD"] = c4.id
  4276. def test_for_each_ref(self) -> None:
  4277. refs = porcelain.for_each_ref(self.repo)
  4278. self.assertEqual(
  4279. [(object_type, tag) for _, object_type, tag in refs],
  4280. [
  4281. (b"commit", b"refs/heads/feat"),
  4282. (b"commit", b"refs/heads/master"),
  4283. (b"commit", b"refs/tags/simple-tag"),
  4284. (b"tag", b"refs/tags/v0.1"),
  4285. (b"tag", b"refs/tags/v1.0"),
  4286. (b"tag", b"refs/tags/v1.1"),
  4287. ],
  4288. )
  4289. def test_for_each_ref_pattern(self) -> None:
  4290. versions = porcelain.for_each_ref(self.repo, pattern="refs/tags/v*")
  4291. self.assertEqual(
  4292. [(object_type, tag) for _, object_type, tag in versions],
  4293. [
  4294. (b"tag", b"refs/tags/v0.1"),
  4295. (b"tag", b"refs/tags/v1.0"),
  4296. (b"tag", b"refs/tags/v1.1"),
  4297. ],
  4298. )
  4299. versions = porcelain.for_each_ref(self.repo, pattern="refs/tags/v1.?")
  4300. self.assertEqual(
  4301. [(object_type, tag) for _, object_type, tag in versions],
  4302. [
  4303. (b"tag", b"refs/tags/v1.0"),
  4304. (b"tag", b"refs/tags/v1.1"),
  4305. ],
  4306. )
  4307. class SparseCheckoutTests(PorcelainTestCase):
  4308. """Integration tests for Dulwich's sparse checkout feature."""
  4309. # NOTE: We do NOT override `setUp()` here because the parent class
  4310. # (PorcelainTestCase) already:
  4311. # 1) Creates self.test_dir = a unique temp dir
  4312. # 2) Creates a subdir named "repo"
  4313. # 3) Calls Repo.init() on that path
  4314. # Re-initializing again caused FileExistsError.
  4315. #
  4316. # Utility/Placeholder
  4317. #
  4318. def sparse_checkout(self, repo, patterns, force=False):
  4319. """Wrapper around the actual porcelain.sparse_checkout function
  4320. to handle any test-specific setup or logging.
  4321. """
  4322. return porcelain.sparse_checkout(repo, patterns, force=force)
  4323. def _write_file(self, rel_path, content):
  4324. """Helper to write a file in the repository working tree."""
  4325. abs_path = os.path.join(self.repo_path, rel_path)
  4326. os.makedirs(os.path.dirname(abs_path), exist_ok=True)
  4327. with open(abs_path, "w") as f:
  4328. f.write(content)
  4329. return abs_path
  4330. def _commit_file(self, rel_path, content):
  4331. """Helper to write, add, and commit a file."""
  4332. abs_path = self._write_file(rel_path, content)
  4333. add(self.repo_path, paths=[abs_path])
  4334. commit(self.repo_path, message=b"Added " + rel_path.encode("utf-8"))
  4335. def _list_wtree_files(self):
  4336. """Return a set of all files (not dirs) present
  4337. in the working tree, ignoring .git/.
  4338. """
  4339. found_files = set()
  4340. for root, dirs, files in os.walk(self.repo_path):
  4341. # Skip .git in the walk
  4342. if ".git" in dirs:
  4343. dirs.remove(".git")
  4344. for filename in files:
  4345. file_rel = os.path.relpath(os.path.join(root, filename), self.repo_path)
  4346. found_files.add(file_rel)
  4347. return found_files
  4348. def test_only_included_paths_appear_in_wtree(self):
  4349. """Only included paths remain in the working tree, excluded paths are removed.
  4350. Commits two files, "keep_me.txt" and "exclude_me.txt". Then applies a
  4351. sparse-checkout pattern containing only "keep_me.txt". Ensures that
  4352. the latter remains in the working tree, while "exclude_me.txt" is
  4353. removed. This verifies correct application of sparse-checkout patterns
  4354. to remove files not listed.
  4355. """
  4356. self._commit_file("keep_me.txt", "I'll stay\n")
  4357. self._commit_file("exclude_me.txt", "I'll be excluded\n")
  4358. patterns = ["keep_me.txt"]
  4359. self.sparse_checkout(self.repo, patterns)
  4360. actual_files = self._list_wtree_files()
  4361. expected_files = {"keep_me.txt"}
  4362. self.assertEqual(
  4363. expected_files,
  4364. actual_files,
  4365. f"Expected only {expected_files}, but found {actual_files}",
  4366. )
  4367. def test_previously_included_paths_become_excluded(self):
  4368. """Previously included files become excluded after pattern changes.
  4369. Verifies that files initially brought into the working tree (e.g.,
  4370. by including `data/`) can later be excluded by narrowing the
  4371. sparse-checkout pattern to just `data/included_1.txt`. Confirms that
  4372. the file `data/included_2.txt` remains in the index with
  4373. skip-worktree set (rather than being removed entirely), ensuring
  4374. data is not lost and Dulwich correctly updates the index flags.
  4375. """
  4376. self._commit_file("data/included_1.txt", "some content\n")
  4377. self._commit_file("data/included_2.txt", "other content\n")
  4378. initial_patterns = ["data/"]
  4379. self.sparse_checkout(self.repo, initial_patterns)
  4380. updated_patterns = ["data/included_1.txt"]
  4381. self.sparse_checkout(self.repo, updated_patterns)
  4382. actual_files = self._list_wtree_files()
  4383. expected_files = {os.path.join("data", "included_1.txt")}
  4384. self.assertEqual(expected_files, actual_files)
  4385. idx = self.repo.open_index()
  4386. self.assertIn(b"data/included_2.txt", idx)
  4387. entry = idx[b"data/included_2.txt"]
  4388. self.assertTrue(entry.skip_worktree)
  4389. def test_force_removes_local_changes_for_excluded_paths(self):
  4390. """Forced sparse checkout removes local modifications for newly excluded paths.
  4391. Verifies that specifying force=True allows destructive operations
  4392. which discard uncommitted changes. First, we commit "file1.txt" and
  4393. then modify it. Next, we apply a pattern that excludes the file,
  4394. using force=True. The local modifications (and the file) should
  4395. be removed, leaving the working tree empty.
  4396. """
  4397. self._commit_file("file1.txt", "original content\n")
  4398. file1_path = os.path.join(self.repo_path, "file1.txt")
  4399. with open(file1_path, "a") as f:
  4400. f.write("local changes!\n")
  4401. new_patterns = ["some_other_file.txt"]
  4402. self.sparse_checkout(self.repo, new_patterns, force=True)
  4403. actual_files = self._list_wtree_files()
  4404. self.assertEqual(
  4405. set(),
  4406. actual_files,
  4407. "Force-sparse-checkout did not remove file with local changes.",
  4408. )
  4409. def test_destructive_refuse_uncommitted_changes_without_force(self):
  4410. """Fail on uncommitted changes for newly excluded paths without force.
  4411. Ensures that a sparse checkout is blocked if it would remove local
  4412. modifications from the working tree. We commit 'config.yaml', then
  4413. modify it, and finally attempt to exclude it via new patterns without
  4414. using force=True. This should raise a CheckoutError rather than
  4415. discarding the local changes.
  4416. """
  4417. self._commit_file("config.yaml", "initial\n")
  4418. cfg_path = os.path.join(self.repo_path, "config.yaml")
  4419. with open(cfg_path, "a") as f:
  4420. f.write("local modifications\n")
  4421. exclude_patterns = ["docs/"]
  4422. with self.assertRaises(CheckoutError):
  4423. self.sparse_checkout(self.repo, exclude_patterns, force=False)
  4424. def test_fnmatch_gitignore_pattern_expansion(self):
  4425. """Reading/writing patterns align with gitignore/fnmatch expansions.
  4426. Ensures that `sparse_checkout` interprets wildcard patterns (like `*.py`)
  4427. in the same way Git's sparse-checkout would. Multiple files are committed
  4428. to `src/` (e.g. `foo.py`, `foo_test.py`, `foo_helper.py`) and to `docs/`.
  4429. Then the pattern `src/foo*.py` is applied, confirming that only the
  4430. matching Python files remain in the working tree while the Markdown file
  4431. under `docs/` is excluded.
  4432. Finally, verifies that the `.git/info/sparse-checkout` file contains the
  4433. specified wildcard pattern (`src/foo*.py`), ensuring correct round-trip
  4434. of user-supplied patterns.
  4435. """
  4436. self._commit_file("src/foo.py", "print('hello')\n")
  4437. self._commit_file("src/foo_test.py", "print('test')\n")
  4438. self._commit_file("docs/readme.md", "# docs\n")
  4439. self._commit_file("src/foo_helper.py", "print('helper')\n")
  4440. patterns = ["src/foo*.py"]
  4441. self.sparse_checkout(self.repo, patterns)
  4442. actual_files = self._list_wtree_files()
  4443. expected_files = {
  4444. os.path.join("src", "foo.py"),
  4445. os.path.join("src", "foo_test.py"),
  4446. os.path.join("src", "foo_helper.py"),
  4447. }
  4448. self.assertEqual(
  4449. expected_files,
  4450. actual_files,
  4451. "Wildcard pattern not matched as expected. Either too strict or too broad.",
  4452. )
  4453. sc_file = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  4454. self.assertTrue(os.path.isfile(sc_file))
  4455. with open(sc_file) as f:
  4456. lines = f.read().strip().split()
  4457. self.assertIn("src/foo*.py", lines)
  4458. class ConeModeTests(PorcelainTestCase):
  4459. """Provide integration tests for Dulwich's cone mode sparse checkout.
  4460. This test suite verifies the expected behavior for:
  4461. * cone_mode_init
  4462. * cone_mode_set
  4463. * cone_mode_add
  4464. Although Dulwich does not yet implement cone mode, these tests are
  4465. prepared in advance to guide future development.
  4466. """
  4467. def setUp(self):
  4468. """Set up a fresh repository for each test.
  4469. This method creates a new empty repo_path and Repo object
  4470. as provided by the PorcelainTestCase base class.
  4471. """
  4472. super().setUp()
  4473. def _commit_file(self, rel_path, content=b"contents"):
  4474. """Add a file at the given relative path and commit it.
  4475. Creates necessary directories, writes the file content,
  4476. stages, and commits. The commit message and author/committer
  4477. are also provided.
  4478. """
  4479. full_path = os.path.join(self.repo_path, rel_path)
  4480. os.makedirs(os.path.dirname(full_path), exist_ok=True)
  4481. with open(full_path, "wb") as f:
  4482. f.write(content)
  4483. porcelain.add(self.repo_path, paths=[full_path])
  4484. porcelain.commit(
  4485. self.repo_path,
  4486. message=b"Adding " + rel_path.encode("utf-8"),
  4487. author=b"Test Author <author@example.com>",
  4488. committer=b"Test Committer <committer@example.com>",
  4489. )
  4490. def _list_wtree_files(self):
  4491. """Return a set of all file paths relative to the repository root.
  4492. Walks the working tree, skipping the .git directory.
  4493. """
  4494. found_files = set()
  4495. for root, dirs, files in os.walk(self.repo_path):
  4496. if ".git" in dirs:
  4497. dirs.remove(".git")
  4498. for fn in files:
  4499. relp = os.path.relpath(os.path.join(root, fn), self.repo_path)
  4500. found_files.add(relp)
  4501. return found_files
  4502. def test_init_excludes_everything(self):
  4503. """Verify that cone_mode_init writes minimal patterns and empties the working tree.
  4504. Make some dummy files, commit them, then call cone_mode_init. Confirm
  4505. that the working tree is empty, the sparse-checkout file has the
  4506. minimal patterns (/*, !/*/), and the relevant config values are set.
  4507. """
  4508. self._commit_file("docs/readme.md", b"# doc\n")
  4509. self._commit_file("src/main.py", b"print('hello')\n")
  4510. porcelain.cone_mode_init(self.repo)
  4511. actual_files = self._list_wtree_files()
  4512. self.assertEqual(
  4513. set(),
  4514. actual_files,
  4515. "cone_mode_init did not exclude all files from the working tree.",
  4516. )
  4517. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  4518. with open(sp_path) as f:
  4519. lines = [ln.strip() for ln in f if ln.strip()]
  4520. self.assertIn("/*", lines)
  4521. self.assertIn("!/*/", lines)
  4522. config = self.repo.get_config()
  4523. self.assertEqual(config.get((b"core",), b"sparseCheckout"), b"true")
  4524. self.assertEqual(config.get((b"core",), b"sparseCheckoutCone"), b"true")
  4525. def test_set_specific_dirs(self):
  4526. """Verify that cone_mode_set overwrites the included directories to only the specified ones.
  4527. Initializes cone mode, commits some files, then calls cone_mode_set with
  4528. a list of directories. Expects that only those directories remain in the
  4529. working tree.
  4530. """
  4531. porcelain.cone_mode_init(self.repo)
  4532. self._commit_file("docs/readme.md", b"# doc\n")
  4533. self._commit_file("src/main.py", b"print('hello')\n")
  4534. self._commit_file("tests/test_foo.py", b"# tests\n")
  4535. # Everything is still excluded initially by init.
  4536. porcelain.cone_mode_set(self.repo, dirs=["docs", "src"])
  4537. actual_files = self._list_wtree_files()
  4538. expected_files = {
  4539. os.path.join("docs", "readme.md"),
  4540. os.path.join("src", "main.py"),
  4541. }
  4542. self.assertEqual(
  4543. expected_files,
  4544. actual_files,
  4545. "Did not see only the 'docs/' and 'src/' dirs in the working tree.",
  4546. )
  4547. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  4548. with open(sp_path) as f:
  4549. lines = [ln.strip() for ln in f if ln.strip()]
  4550. # For standard cone mode, we'd expect lines like:
  4551. # /* (include top-level files)
  4552. # !/*/ (exclude subdirectories)
  4553. # !/docs/ (re-include docs)
  4554. # !/src/ (re-include src)
  4555. # Instead of the wildcard-based lines the old test used.
  4556. self.assertIn("/*", lines)
  4557. self.assertIn("!/*/", lines)
  4558. self.assertIn("/docs/", lines)
  4559. self.assertIn("/src/", lines)
  4560. self.assertNotIn("/tests/", lines)
  4561. def test_set_overwrites_old_dirs(self):
  4562. """Ensure that calling cone_mode_set again overwrites old includes.
  4563. Initializes cone mode, includes two directories, then calls
  4564. cone_mode_set again with a different directory to confirm the
  4565. new set of includes replaces the old.
  4566. """
  4567. porcelain.cone_mode_init(self.repo)
  4568. self._commit_file("docs/readme.md")
  4569. self._commit_file("src/main.py")
  4570. self._commit_file("tests/test_bar.py")
  4571. porcelain.cone_mode_set(self.repo, dirs=["docs", "src"])
  4572. self.assertEqual(
  4573. {os.path.join("docs", "readme.md"), os.path.join("src", "main.py")},
  4574. self._list_wtree_files(),
  4575. )
  4576. # Overwrite includes, now only 'tests'
  4577. porcelain.cone_mode_set(self.repo, dirs=["tests"], force=True)
  4578. actual_files = self._list_wtree_files()
  4579. expected_files = {os.path.join("tests", "test_bar.py")}
  4580. self.assertEqual(expected_files, actual_files)
  4581. def test_force_removal_of_local_mods(self):
  4582. """Confirm that force=True removes local changes in excluded paths.
  4583. cone_mode_init and cone_mode_set are called, a file is locally modified,
  4584. and then cone_mode_set is called again with force=True to exclude that path.
  4585. The excluded file should be removed with no CheckoutError.
  4586. """
  4587. porcelain.cone_mode_init(self.repo)
  4588. porcelain.cone_mode_set(self.repo, dirs=["docs"])
  4589. self._commit_file("docs/readme.md", b"Docs stuff\n")
  4590. self._commit_file("src/main.py", b"print('hello')\n")
  4591. # Modify src/main.py
  4592. with open(os.path.join(self.repo_path, "src/main.py"), "ab") as f:
  4593. f.write(b"extra line\n")
  4594. # Exclude src/ with force=True
  4595. porcelain.cone_mode_set(self.repo, dirs=["docs"], force=True)
  4596. actual_files = self._list_wtree_files()
  4597. expected_files = {os.path.join("docs", "readme.md")}
  4598. self.assertEqual(expected_files, actual_files)
  4599. def test_add_and_merge_dirs(self):
  4600. """Verify that cone_mode_add merges new directories instead of overwriting them.
  4601. After initializing cone mode and including a single directory, call
  4602. cone_mode_add with a new directory. Confirm that both directories
  4603. remain included. Repeat for an additional directory to ensure it
  4604. is merged, not overwritten.
  4605. """
  4606. porcelain.cone_mode_init(self.repo)
  4607. self._commit_file("docs/readme.md", b"# doc\n")
  4608. self._commit_file("src/main.py", b"print('hello')\n")
  4609. self._commit_file("tests/test_bar.py", b"# tests\n")
  4610. # Include "docs" only
  4611. porcelain.cone_mode_set(self.repo, dirs=["docs"])
  4612. self.assertEqual({os.path.join("docs", "readme.md")}, self._list_wtree_files())
  4613. # Add "src"
  4614. porcelain.cone_mode_add(self.repo, dirs=["src"])
  4615. actual_files = self._list_wtree_files()
  4616. self.assertEqual(
  4617. {os.path.join("docs", "readme.md"), os.path.join("src", "main.py")},
  4618. actual_files,
  4619. )
  4620. # Add "tests" as well
  4621. porcelain.cone_mode_add(self.repo, dirs=["tests"])
  4622. actual_files = self._list_wtree_files()
  4623. expected_files = {
  4624. os.path.join("docs", "readme.md"),
  4625. os.path.join("src", "main.py"),
  4626. os.path.join("tests", "test_bar.py"),
  4627. }
  4628. self.assertEqual(expected_files, actual_files)
  4629. # Check .git/info/sparse-checkout
  4630. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  4631. with open(sp_path) as f:
  4632. lines = [ln.strip() for ln in f if ln.strip()]
  4633. # Standard cone mode lines:
  4634. # "/*" -> include top-level
  4635. # "!/*/" -> exclude subdirectories
  4636. # "!/docs/", "!/src/", "!/tests/" -> re-include the directories we added
  4637. self.assertIn("/*", lines)
  4638. self.assertIn("!/*/", lines)
  4639. self.assertIn("/docs/", lines)
  4640. self.assertIn("/src/", lines)
  4641. self.assertIn("/tests/", lines)
  4642. class UnpackObjectsTest(PorcelainTestCase):
  4643. def test_unpack_objects(self):
  4644. """Test unpacking objects from a pack file."""
  4645. # Create a test repository with some objects
  4646. b1 = Blob()
  4647. b1.data = b"test content 1"
  4648. b2 = Blob()
  4649. b2.data = b"test content 2"
  4650. # Add objects to the repo
  4651. self.repo.object_store.add_object(b1)
  4652. self.repo.object_store.add_object(b2)
  4653. # Create a pack file with these objects
  4654. pack_path = os.path.join(self.test_dir, "test_pack")
  4655. with (
  4656. open(pack_path + ".pack", "wb") as pack_f,
  4657. open(pack_path + ".idx", "wb") as idx_f,
  4658. ):
  4659. porcelain.pack_objects(
  4660. self.repo,
  4661. [b1.id, b2.id],
  4662. pack_f,
  4663. idx_f,
  4664. )
  4665. # Create a new repository to unpack into
  4666. target_repo_path = os.path.join(self.test_dir, "target_repo")
  4667. target_repo = Repo.init(target_repo_path, mkdir=True)
  4668. self.addCleanup(target_repo.close)
  4669. # Unpack the objects
  4670. count = porcelain.unpack_objects(pack_path + ".pack", target_repo_path)
  4671. # Verify the objects were unpacked
  4672. self.assertEqual(2, count)
  4673. self.assertIn(b1.id, target_repo.object_store)
  4674. self.assertIn(b2.id, target_repo.object_store)
  4675. # Verify the content is correct
  4676. unpacked_b1 = target_repo.object_store[b1.id]
  4677. unpacked_b2 = target_repo.object_store[b2.id]
  4678. self.assertEqual(b1.data, unpacked_b1.data)
  4679. self.assertEqual(b2.data, unpacked_b2.data)
  4680. class CountObjectsTests(PorcelainTestCase):
  4681. def test_count_objects_empty_repo(self):
  4682. """Test counting objects in an empty repository."""
  4683. stats = porcelain.count_objects(self.repo)
  4684. self.assertEqual(0, stats.count)
  4685. self.assertEqual(0, stats.size)
  4686. def test_count_objects_verbose_empty_repo(self):
  4687. """Test verbose counting in an empty repository."""
  4688. stats = porcelain.count_objects(self.repo, verbose=True)
  4689. self.assertEqual(0, stats.count)
  4690. self.assertEqual(0, stats.size)
  4691. self.assertEqual(0, stats.in_pack)
  4692. self.assertEqual(0, stats.packs)
  4693. self.assertEqual(0, stats.size_pack)
  4694. def test_count_objects_with_loose_objects(self):
  4695. """Test counting loose objects."""
  4696. # Create some loose objects
  4697. blob1 = make_object(Blob, data=b"data1")
  4698. blob2 = make_object(Blob, data=b"data2")
  4699. self.repo.object_store.add_object(blob1)
  4700. self.repo.object_store.add_object(blob2)
  4701. stats = porcelain.count_objects(self.repo)
  4702. self.assertEqual(2, stats.count)
  4703. self.assertGreater(stats.size, 0)
  4704. def test_count_objects_verbose_with_objects(self):
  4705. """Test verbose counting with both loose and packed objects."""
  4706. # Add some loose objects
  4707. for i in range(3):
  4708. blob = make_object(Blob, data=f"data{i}".encode())
  4709. self.repo.object_store.add_object(blob)
  4710. # Create a simple commit to have some objects in a pack
  4711. tree = Tree()
  4712. c1 = make_commit(tree=tree.id, message=b"Test commit")
  4713. self.repo.object_store.add_objects([(tree, None), (c1, None)])
  4714. self.repo.refs[b"HEAD"] = c1.id
  4715. # Repack to create a pack file
  4716. porcelain.repack(self.repo)
  4717. stats = porcelain.count_objects(self.repo, verbose=True)
  4718. # After repacking, loose objects might be cleaned up
  4719. self.assertIsInstance(stats.count, int)
  4720. self.assertIsInstance(stats.size, int)
  4721. self.assertGreater(stats.in_pack, 0) # Should have packed objects
  4722. self.assertGreater(stats.packs, 0) # Should have at least one pack
  4723. self.assertGreater(stats.size_pack, 0) # Pack should have size
  4724. # Verify it's the correct dataclass type
  4725. self.assertIsInstance(stats, CountObjectsResult)
  4726. class PruneTests(PorcelainTestCase):
  4727. def test_prune_removes_old_tempfiles(self):
  4728. """Test that prune removes old temporary files."""
  4729. # Create an old temporary file in the objects directory
  4730. objects_dir = os.path.join(self.repo.path, ".git", "objects")
  4731. tmp_pack_path = os.path.join(objects_dir, "tmp_pack_test")
  4732. with open(tmp_pack_path, "wb") as f:
  4733. f.write(b"old temporary data")
  4734. # Make it old
  4735. old_time = time.time() - (DEFAULT_TEMPFILE_GRACE_PERIOD + 3600)
  4736. os.utime(tmp_pack_path, (old_time, old_time))
  4737. # Run prune
  4738. porcelain.prune(self.repo.path)
  4739. # Verify the file was removed
  4740. self.assertFalse(os.path.exists(tmp_pack_path))
  4741. def test_prune_keeps_recent_tempfiles(self):
  4742. """Test that prune keeps recent temporary files."""
  4743. # Create a recent temporary file
  4744. objects_dir = os.path.join(self.repo.path, ".git", "objects")
  4745. tmp_pack_path = os.path.join(objects_dir, "tmp_pack_recent")
  4746. with open(tmp_pack_path, "wb") as f:
  4747. f.write(b"recent temporary data")
  4748. self.addCleanup(os.remove, tmp_pack_path)
  4749. # Run prune
  4750. porcelain.prune(self.repo.path)
  4751. # Verify the file was NOT removed
  4752. self.assertTrue(os.path.exists(tmp_pack_path))
  4753. def test_prune_with_custom_grace_period(self):
  4754. """Test prune with custom grace period."""
  4755. # Create a 1-hour-old temporary file
  4756. objects_dir = os.path.join(self.repo.path, ".git", "objects")
  4757. tmp_pack_path = os.path.join(objects_dir, "tmp_pack_1hour")
  4758. with open(tmp_pack_path, "wb") as f:
  4759. f.write(b"1 hour old data")
  4760. # Make it 1 hour old
  4761. old_time = time.time() - 3600
  4762. os.utime(tmp_pack_path, (old_time, old_time))
  4763. # Prune with 30-minute grace period should remove it
  4764. porcelain.prune(self.repo.path, grace_period=1800)
  4765. # Verify the file was removed
  4766. self.assertFalse(os.path.exists(tmp_pack_path))
  4767. def test_prune_dry_run(self):
  4768. """Test prune in dry-run mode."""
  4769. # Create an old temporary file
  4770. objects_dir = os.path.join(self.repo.path, ".git", "objects")
  4771. tmp_pack_path = os.path.join(objects_dir, "tmp_pack_dryrun")
  4772. with open(tmp_pack_path, "wb") as f:
  4773. f.write(b"old temporary data")
  4774. self.addCleanup(os.remove, tmp_pack_path)
  4775. # Make it old
  4776. old_time = time.time() - (DEFAULT_TEMPFILE_GRACE_PERIOD + 3600)
  4777. os.utime(tmp_pack_path, (old_time, old_time))
  4778. # Run prune in dry-run mode
  4779. porcelain.prune(self.repo.path, dry_run=True)
  4780. # Verify the file was NOT removed (dry run)
  4781. self.assertTrue(os.path.exists(tmp_pack_path))
  4782. class FilterBranchTests(PorcelainTestCase):
  4783. def setUp(self):
  4784. super().setUp()
  4785. # Create initial commits with different authors
  4786. from dulwich.objects import Commit, Tree
  4787. # Create actual tree and blob objects
  4788. tree = Tree()
  4789. self.repo.object_store.add_object(tree)
  4790. c1 = Commit()
  4791. c1.tree = tree.id
  4792. c1.parents = []
  4793. c1.author = b"Old Author <old@example.com>"
  4794. c1.author_time = 1000
  4795. c1.author_timezone = 0
  4796. c1.committer = b"Old Committer <old@example.com>"
  4797. c1.commit_time = 1000
  4798. c1.commit_timezone = 0
  4799. c1.message = b"Initial commit"
  4800. self.repo.object_store.add_object(c1)
  4801. c2 = Commit()
  4802. c2.tree = tree.id
  4803. c2.parents = [c1.id]
  4804. c2.author = b"Another Author <another@example.com>"
  4805. c2.author_time = 2000
  4806. c2.author_timezone = 0
  4807. c2.committer = b"Another Committer <another@example.com>"
  4808. c2.commit_time = 2000
  4809. c2.commit_timezone = 0
  4810. c2.message = b"Second commit\n\nWith body"
  4811. self.repo.object_store.add_object(c2)
  4812. c3 = Commit()
  4813. c3.tree = tree.id
  4814. c3.parents = [c2.id]
  4815. c3.author = b"Third Author <third@example.com>"
  4816. c3.author_time = 3000
  4817. c3.author_timezone = 0
  4818. c3.committer = b"Third Committer <third@example.com>"
  4819. c3.commit_time = 3000
  4820. c3.commit_timezone = 0
  4821. c3.message = b"Third commit"
  4822. self.repo.object_store.add_object(c3)
  4823. self.repo.refs[b"refs/heads/master"] = c3.id
  4824. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  4825. # Store IDs for test assertions
  4826. self.c1_id = c1.id
  4827. self.c2_id = c2.id
  4828. self.c3_id = c3.id
  4829. def test_filter_branch_author(self):
  4830. """Test filtering branch with author changes."""
  4831. def filter_author(author):
  4832. # Change all authors to "New Author"
  4833. return b"New Author <new@example.com>"
  4834. result = porcelain.filter_branch(
  4835. self.repo_path, "master", filter_author=filter_author
  4836. )
  4837. # Check that we have mappings for all commits
  4838. self.assertEqual(len(result), 3)
  4839. # Verify the branch ref was updated
  4840. new_head = self.repo.refs[b"refs/heads/master"]
  4841. self.assertNotEqual(new_head, self.c3_id)
  4842. # Verify the original ref was saved
  4843. original_ref = self.repo.refs[b"refs/original/refs/heads/master"]
  4844. self.assertEqual(original_ref, self.c3_id)
  4845. # Check that authors were updated
  4846. new_commit = self.repo[new_head]
  4847. self.assertEqual(new_commit.author, b"New Author <new@example.com>")
  4848. # Check parent chain
  4849. parent = self.repo[new_commit.parents[0]]
  4850. self.assertEqual(parent.author, b"New Author <new@example.com>")
  4851. def test_filter_branch_message(self):
  4852. """Test filtering branch with message changes."""
  4853. def filter_message(message):
  4854. # Add prefix to all messages
  4855. return b"[FILTERED] " + message
  4856. porcelain.filter_branch(self.repo_path, "master", filter_message=filter_message)
  4857. # Verify messages were updated
  4858. new_head = self.repo.refs[b"refs/heads/master"]
  4859. new_commit = self.repo[new_head]
  4860. self.assertTrue(new_commit.message.startswith(b"[FILTERED] "))
  4861. def test_filter_branch_custom_filter(self):
  4862. """Test filtering branch with custom filter function."""
  4863. def custom_filter(commit):
  4864. # Change both author and message
  4865. return {
  4866. "author": b"Custom Author <custom@example.com>",
  4867. "message": b"Custom: " + commit.message,
  4868. }
  4869. porcelain.filter_branch(self.repo_path, "master", filter_fn=custom_filter)
  4870. # Verify custom filter was applied
  4871. new_head = self.repo.refs[b"refs/heads/master"]
  4872. new_commit = self.repo[new_head]
  4873. self.assertEqual(new_commit.author, b"Custom Author <custom@example.com>")
  4874. self.assertTrue(new_commit.message.startswith(b"Custom: "))
  4875. def test_filter_branch_no_changes(self):
  4876. """Test filtering branch with no changes."""
  4877. result = porcelain.filter_branch(self.repo_path, "master")
  4878. # All commits should map to themselves
  4879. for old_sha, new_sha in result.items():
  4880. self.assertEqual(old_sha, new_sha)
  4881. # HEAD should be unchanged
  4882. self.assertEqual(self.repo.refs[b"refs/heads/master"], self.c3_id)
  4883. def test_filter_branch_force(self):
  4884. """Test force filtering a previously filtered branch."""
  4885. # First filter
  4886. porcelain.filter_branch(
  4887. self.repo_path, "master", filter_message=lambda m: b"First: " + m
  4888. )
  4889. # Try again without force - should fail
  4890. with self.assertRaises(porcelain.Error):
  4891. porcelain.filter_branch(
  4892. self.repo_path, "master", filter_message=lambda m: b"Second: " + m
  4893. )
  4894. # Try again with force - should succeed
  4895. porcelain.filter_branch(
  4896. self.repo_path,
  4897. "master",
  4898. filter_message=lambda m: b"Second: " + m,
  4899. force=True,
  4900. )
  4901. # Verify second filter was applied
  4902. new_head = self.repo.refs[b"refs/heads/master"]
  4903. new_commit = self.repo[new_head]
  4904. self.assertTrue(new_commit.message.startswith(b"Second: First: "))