test_porcelain.py 162 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500
  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.objects import ZERO_SHA, Blob, Tag, Tree
  40. from dulwich.porcelain import (
  41. CheckoutError, # Hypothetical or real error class
  42. add,
  43. commit,
  44. )
  45. from dulwich.repo import NoIndexPresent, Repo
  46. from dulwich.server import DictBackend
  47. from dulwich.tests.utils import build_commit_graph, make_commit, make_object
  48. from dulwich.web import make_server, make_wsgi_chain
  49. from . import TestCase
  50. try:
  51. import gpg
  52. except ImportError:
  53. gpg = None
  54. def flat_walk_dir(dir_to_walk):
  55. for dirpath, _, filenames in os.walk(dir_to_walk):
  56. rel_dirpath = os.path.relpath(dirpath, dir_to_walk)
  57. if not dirpath == dir_to_walk:
  58. yield rel_dirpath
  59. for filename in filenames:
  60. if dirpath == dir_to_walk:
  61. yield filename
  62. else:
  63. yield os.path.join(rel_dirpath, filename)
  64. class PorcelainTestCase(TestCase):
  65. def setUp(self) -> None:
  66. super().setUp()
  67. self.test_dir = tempfile.mkdtemp()
  68. self.addCleanup(shutil.rmtree, self.test_dir)
  69. self.repo_path = os.path.join(self.test_dir, "repo")
  70. self.repo = Repo.init(self.repo_path, mkdir=True)
  71. self.addCleanup(self.repo.close)
  72. def assertRecentTimestamp(self, ts) -> None:
  73. # On some slow CIs it does actually take more than 5 seconds to go from
  74. # creating the tag to here.
  75. self.assertLess(time.time() - ts, 50)
  76. @skipIf(gpg is None, "gpg is not available")
  77. class PorcelainGpgTestCase(PorcelainTestCase):
  78. DEFAULT_KEY = """
  79. -----BEGIN PGP PRIVATE KEY BLOCK-----
  80. lQVYBGBjIyIBDADAwydvMPQqeEiK54FG1DHwT5sQejAaJOb+PsOhVa4fLcKsrO3F
  81. g5CxO+/9BHCXAr8xQAtp/gOhDN05fyK3MFyGlL9s+Cd8xf34S3R4rN/qbF0oZmaa
  82. FW0MuGnniq54HINs8KshadVn1Dhi/GYSJ588qNFRl/qxFTYAk+zaGsgX/QgFfy0f
  83. djWXJLypZXu9D6DlyJ0cPSzUlfBkI2Ytx6grzIquRjY0FbkjK3l+iGsQ+ebRMdcP
  84. Sqd5iTN9XuzIUVoBFAZBRjibKV3N2wxlnCbfLlzCyDp7rktzSThzjJ2pVDuLrMAx
  85. 6/L9hIhwmFwdtY4FBFGvMR0b0Ugh3kCsRWr8sgj9I7dUoLHid6ObYhJFhnD3GzRc
  86. U+xX1uy3iTCqJDsG334aQIhC5Giuxln4SUZna2MNbq65ksh38N1aM/t3+Dc/TKVB
  87. rb5KWicRPCQ4DIQkHMDCSPyj+dvRLCPzIaPvHD7IrCfHYHOWuvvPGCpwjo0As3iP
  88. IecoMeguPLVaqgcAEQEAAQAL/i5/pQaUd4G7LDydpbixPS6r9UrfPrU/y5zvBP/p
  89. DCynPDutJ1oq539pZvXQ2VwEJJy7x0UVKkjyMndJLNWly9wHC7o8jkHx/NalVP47
  90. LXR+GWbCdOOcYYbdAWcCNB3zOtzPnWhdAEagkc2G9xRQDIB0dLHLCIUpCbLP/CWM
  91. qlHnDsVMrVTWjgzcpsnyGgw8NeLYJtYGB8dsN+XgCCjo7a9LEvUBKNgdmWBbf14/
  92. iBw7PCugazFcH9QYfZwzhsi3nqRRagTXHbxFRG0LD9Ro9qCEutHYGP2PJ59Nj8+M
  93. zaVkJj/OxWxVOGvn2q16mQBCjKpbWfqXZVVl+G5DGOmiSTZqXy+3j6JCKdOMy6Qd
  94. JBHOHhFZXYmWYaaPzoc33T/C3QhMfY5sOtUDLJmV05Wi4dyBeNBEslYgUuTk/jXb
  95. 5ZAie25eDdrsoqkcnSs2ZguMF7AXhe6il2zVhUUMs/6UZgd6I7I4Is0HXT/pnxEp
  96. uiTRFu4v8E+u+5a8O3pffe5boQYA3TsIxceen20qY+kRaTOkURHMZLn/y6KLW8bZ
  97. rNJyXWS9hBAcbbSGhfOwYfzbDCM17yPQO3E2zo8lcGdRklUdIIaCxQwtu36N5dfx
  98. OLCCQc5LmYdl/EAm91iAhrr7dNntZ18MU09gdzUu+ONZwu4CP3cJT83+qYZULso8
  99. 4Fvd/X8IEfGZ7kM+ylrdqBwtlrn8yYXtom+ows2M2UuNR53B+BUOd73kVLTkTCjE
  100. JH63+nE8BqG7tDLCMws+23SAA3xxBgDfDrr0x7zCozQKVQEqBzQr9Uoo/c/ZjAfi
  101. syzNSrDz+g5gqJYtuL9XpPJVWf6V1GXVyJlSbxR9CjTkBxmlPxpvV25IsbVSsh0o
  102. aqkf2eWpbCL6Qb2E0jd1rvf8sGeTTohzYfiSVVsC2t9ngRO/CmetizwQBvRzLGMZ
  103. 4mtAPiy7ZEDc2dFrPp7zlKISYmJZUx/DJVuZWuOrVMpBP+bSgJXoMTlICxZUqUnE
  104. 2VKVStb/L+Tl8XCwIWdrZb9BaDnHqfcGAM2B4HNPxP88Yj1tEDly/vqeb3vVMhj+
  105. S1lunnLdgxp46YyuTMYAzj88eCGurRtzBsdxxlGAsioEnZGebEqAHQbieKq/DO6I
  106. MOMZHMSVBDqyyIx3assGlxSX8BSFW0lhKyT7i0XqnAgCJ9f/5oq0SbFGq+01VQb7
  107. jIx9PbcYJORxsE0JG/CXXPv27bRtQXsudkWGSYvC0NLOgk4z8+kQpQtyFh16lujq
  108. WRwMeriu0qNDjCa1/eHIKDovhAZ3GyO5/9m1tBlUZXN0IFVzZXIgPHRlc3RAdGVz
  109. dC5jb20+iQHOBBMBCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEjrR8
  110. MQ4fJK44PYMvfN2AClLmXiYFAmDcEZEACgkQfN2AClLmXibZzgv/ZfeTpTuqQE1W
  111. C1jT5KpQExnt0BizTX0U7BvSn8Fr6VXTyol6kYc3u71GLUuJyawCLtIzOXqOXJvz
  112. bjcZqymcMADuftKcfMy513FhbF6MhdVd6QoeBP6+7/xXOFJCi+QVYF7SQ2h7K1Qm
  113. +yXOiAMgSxhCZQGPBNJLlDUOd47nSIMANvlumFtmLY/1FD7RpG7WQWjeX1mnxNTw
  114. hUU+Yv7GuFc/JprXCIYqHbhWfvXyVtae2ZK4xuVi5eqwA2RfggOVM7drb+CgPhG0
  115. +9aEDDLOZqVi65wK7J73Puo3rFTbPQMljxw5s27rWqF+vB6hhVdJOPNomWy3naPi
  116. k5MW0mhsacASz1WYndpZz+XaQTq/wJF5HUyyeUWJ0vlOEdwx021PHcqSTyfNnkjD
  117. KncrE21t2sxWRsgGDETxIwkd2b2HNGAvveUD0ffFK/oJHGSXjAERFGc3wuiDj3mQ
  118. BvKm4wt4QF9ZMrCdhMAA6ax5kfEUqQR4ntmrJk/khp/mV7TILaI4nQVYBGBjIyIB
  119. DADghIo9wXnRxzfdDTvwnP8dHpLAIaPokgdpyLswqUCixJWiW2xcV6weUjEWwH6n
  120. eN/t1uZYVehbrotxVPla+MPvzhxp6/cmG+2lhzEBOp6zRwnL1wIB6HoKJfpREhyM
  121. c8rLR0zMso1L1bJTyydvnu07a7BWo3VWKjilb0rEZZUSD/2hidx5HxMOJSoidLWe
  122. d/PPuv6yht3NtA4UThlcfldm9G6PbqCdm1kMEKAkq0wVJvhPJ6gEFRNJimgygfUw
  123. MDFXEIhQtxjgdV5Uoz3O5452VLoRsDlgpi3E0WDGj7WXDaO5uSU0T5aJgVgHCP/f
  124. xZhHuQFk2YYIl5nCBpOZyWWI0IKmscTuEwzpkhICQDQFvcMZ5ibsl7wA2P7YTrQf
  125. FDMjjzuaK80GYPfxDFlyKUyLqFt8w/QzsZLDLX7+jxIEpbRAaMw/JsWqm5BMxxbS
  126. 3CIQiS5S3oSKDsNINelqWFfwvLhvlQra8gIxyNTlek25OdgG66BiiX+seH8A/ql+
  127. F+MAEQEAAQAL/1jrNSLjMt9pwo6qFKClVQZP2vf7+sH7v7LeHIDXr3EnYUnVYnOq
  128. B1FU5PspTp/+J9W25DB9CZLx7Gj8qeslFdiuLSOoIBB4RCToB3kAoeTH0DHqW/Gs
  129. hFTrmJkuDp9zpo/ek6SIXJx5rHAyR9KVw0fizQprH2f6PcgLbTWeM61dJuqowmg3
  130. 7eCOyIKv7VQvFqEhYokLD+JNmrvg+Htg0DXGvdjRjAwPf/NezEXpj67a6cHTp1/C
  131. hwp7pevG+3fTxaCJFesl5/TxxtnaBLE8m2uo/S6Hxgn9l0edonroe1QlTjEqGLy2
  132. 7qi2z5Rem+v6GWNDRgvAWur13v8FNdyduHlioG/NgRsU9mE2MYeFsfi3cfNpJQp/
  133. wC9PSCIXrb/45mkS8KyjZpCrIPB9RV/m0MREq01TPom7rstZc4A1pD0Ot7AtUYS3
  134. e95zLyEmeLziPJ9fV4fgPmEudDr1uItnmV0LOskKlpg5sc0hhdrwYoobfkKt2dx6
  135. DqfMlcM1ZkUbLQYA4jwfpFJG4HmYvjL2xCJxM0ycjvMbqFN+4UjgYWVlRfOrm1V4
  136. Op86FjbRbV6OOCNhznotAg7mul4xtzrrTkK8o3YLBeJseDgl4AWuzXtNa9hE0XpK
  137. 9gJoEHUuBOOsamVh2HpXESFyE5CclOV7JSh541TlZKfnqfZYCg4JSbp0UijkawCL
  138. 5bJJUiGGMD9rZUxIAKQO1DvUEzptS7Jl6S3y5sbIIhilp4KfYWbSk3PPu9CnZD5b
  139. LhEQp0elxnb/IL8PBgD+DpTeC8unkGKXUpbe9x0ISI6V1D6FmJq/FxNg7fMa3QCh
  140. fGiAyoTm80ZETynj+blRaDO3gY4lTLa3Opubof1EqK2QmwXmpyvXEZNYcQfQ2CCS
  141. GOWUCK8jEQamUPf1PWndZXJUmROI1WukhlL71V/ir6zQeVCv1wcwPwclJPnAe87u
  142. pEklnCYpvsEldwHUX9u0BWzoULIEsi+ddtHmT0KTeF/DHRy0W15jIHbjFqhqckj1
  143. /6fmr7l7kIi/kN4vWe0F/0Q8IXX+cVMgbl3aIuaGcvENLGcoAsAtPGx88SfRgmfu
  144. HK64Y7hx1m+Bo215rxJzZRjqHTBPp0BmCi+JKkaavIBrYRbsx20gveI4dzhLcUhB
  145. kiT4Q7oz0/VbGHS1CEf9KFeS/YOGj57s4yHauSVI0XdP9kBRTWmXvBkzsooB2cKH
  146. hwhUN7iiT1k717CiTNUT6Q/pcPFCyNuMoBBGQTU206JEgIjQvI3f8xMUMGmGVVQz
  147. 9/k716ycnhb2JZ/Q/AyQIeHJiQG2BBgBCAAgAhsMFiEEjrR8MQ4fJK44PYMvfN2A
  148. ClLmXiYFAmDcEa4ACgkQfN2AClLmXiZxxQv/XaMN0hPCygtrQMbCsTNb34JbvJzh
  149. hngPuUAfTbRHrR3YeATyQofNbL0DD3fvfzeFF8qESqvzCSZxS6dYsXPd4MCJTzlp
  150. zYBZ2X0sOrgDqZvqCZKN72RKgdk0KvthdzAxsIm2dfcQOxxowXMxhJEXZmsFpusx
  151. jKJxOcrfVRjXJnh9isY0NpCoqMQ+3k3wDJ3VGEHV7G+A+vFkWfbLJF5huQ96uaH9
  152. Uc+jUsREUH9G82ZBqpoioEN8Ith4VXpYnKdTMonK/+ZcyeraJZhXrvbjnEomKdzU
  153. 0pu4bt1HlLR3dcnpjN7b009MBf2xLgEfQk2nPZ4zzY+tDkxygtPllaB4dldFjBpT
  154. j7Q+t49sWMjmlJUbLlHfuJ7nUUK5+cGjBsWVObAEcyfemHWCTVFnEa2BJslGC08X
  155. rFcjRRcMEr9ct4551QFBHsv3O/Wp3/wqczYgE9itSnGT05w+4vLt4smG+dnEHjRJ
  156. brMb2upTHa+kjktjdO96/BgSnKYqmNmPB/qB
  157. =ivA/
  158. -----END PGP PRIVATE KEY BLOCK-----
  159. """
  160. DEFAULT_KEY_ID = "8EB47C310E1F24AE383D832F7CDD800A52E65E26"
  161. NON_DEFAULT_KEY = """
  162. -----BEGIN PGP PRIVATE KEY BLOCK-----
  163. lQVYBGBjI0ABDADGWBRp+t02emfzUlhrc1psqIhhecFm6Em0Kv33cfDpnfoMF1tK
  164. Yy/4eLYIR7FmpdbFPcDThFNHbXJzBi00L1mp0XQE2l50h/2bDAAgREdZ+NVo5a7/
  165. RSZjauNU1PxW6pnXMehEh1tyIQmV78jAukaakwaicrpIenMiFUN3fAKHnLuFffA6
  166. t0f3LqJvTDhUw/o2vPgw5e6UDQhA1C+KTv1KXVrhJNo88a3hZqCZ76z3drKR411Q
  167. zYgT4DUb8lfnbN+z2wfqT9oM5cegh2k86/mxAA3BYOeQrhmQo/7uhezcgbxtdGZr
  168. YlbuaNDTSBrn10ZoaxLPo2dJe2zWxgD6MpvsGU1w3tcRW508qo/+xoWp2/pDzmok
  169. +uhOh1NAj9zB05VWBz1r7oBgCOIKpkD/LD4VKq59etsZ/UnrYDwKdXWZp7uhshkU
  170. M7N35lUJcR76a852dlMdrgpmY18+BP7+o7M+5ElHTiqQbMuE1nHTg8RgVpdV+tUx
  171. dg6GWY/XHf5asm8AEQEAAQAL/A85epOp+GnymmEQfI3+5D178D//Lwu9n86vECB6
  172. xAHCqQtdjZnXpDp/1YUsL59P8nzgYRk7SoMskQDoQ/cB/XFuDOhEdMSgHaTVlnrj
  173. ktCCq6rqGnUosyolbb64vIfVaSqd/5SnCStpAsnaBoBYrAu4ZmV4xfjDQWwn0q5s
  174. u+r56mD0SkjPgbwk/b3qTVagVmf2OFzUgWwm1e/X+bA1oPag1NV8VS4hZPXswT4f
  175. qhiyqUFOgP6vUBcqehkjkIDIl/54xII7/P5tp3LIZawvIXqHKNTqYPCqaCqCj+SL
  176. vMYDIb6acjescfZoM71eAeHAANeFZzr/rwfBT+dEP6qKmPXNcvgE11X44ZCr04nT
  177. zOV/uDUifEvKT5qgtyJpSFEVr7EXubJPKoNNhoYqq9z1pYU7IedX5BloiVXKOKTY
  178. 0pk7JkLqf3g5fYtXh/wol1owemITJy5V5PgaqZvk491LkI6S+kWC7ANYUg+TDPIW
  179. afxW3E5N1CYV6XDAl0ZihbLcoQYAy0Ky/p/wayWKePyuPBLwx9O89GSONK2pQljZ
  180. yaAgxPQ5/i1vx6LIMg7k/722bXR9W3zOjWOin4eatPM3d2hkG96HFvnBqXSmXOPV
  181. 03Xqy1/B5Tj8E9naLKUHE/OBQEc363DgLLG9db5HfPlpAngeppYPdyWkhzXyzkgS
  182. PylaE5eW3zkdjEbYJ6RBTecTZEgBaMvJNPdWbn//frpP7kGvyiCg5Es+WjLInUZ6
  183. 0sdifcNTCewzLXK80v/y5mVOdJhPBgD5zs9cYdyiQJayqAuOr+He1eMHMVUbm9as
  184. qBmPrst398eBW9ZYF7eBfTSlUf6B+WnvyLKEGsUf/7IK0EWDlzoBuWzWiHjUAY1g
  185. m9eTV2MnvCCCefqCErWwfFo2nWOasAZA9sKD+ICIBY4tbtvSl4yfLBzTMwSvs9ZS
  186. K1ocPSYUnhm2miSWZ8RLZPH7roHQasNHpyq/AX7DahFf2S/bJ+46ZGZ8Pigr7hA+
  187. MjmpQ4qVdb5SaViPmZhAKO+PjuCHm+EF/2H0Y3Sl4eXgxZWoQVOUeXdWg9eMfYrj
  188. XDtUMIFppV/QxbeztZKvJdfk64vt/crvLsOp0hOky9cKwY89r4QaHfexU3qR+qDq
  189. UlMvR1rHk7dS5HZAtw0xKsFJNkuDxvBkMqv8Los8zp3nUl+U99dfZOArzNkW38wx
  190. FPa0ixkC9za2BkDrWEA8vTnxw0A2upIFegDUhwOByrSyfPPnG3tKGeqt3Izb/kDk
  191. Q9vmo+HgxBOguMIvlzbBfQZwtbd/gXzlvPqCtCJBbm90aGVyIFRlc3QgVXNlciA8
  192. dGVzdDJAdGVzdC5jb20+iQHOBBMBCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B
  193. AheAFiEEapM5P1DF5qzT1vtFuTYhLttOFMAFAmDcEeEACgkQuTYhLttOFMDe0Qv/
  194. Qx/bzXztJ3BCc+CYAVDx7Kr37S68etwwLgcWzhG+CDeMB5F/QE+upKgxy2iaqQFR
  195. mxfOMgf/TIQkUfkbaASzK1LpnesYO85pk7XYjoN1bYEHiXTkeW+bgB6aJIxrRmO2
  196. SrWasdBC/DsI3Mrya8YMt/TiHC6VpRJVxCe5vv7/kZC4CXrgTBnZocXx/YXimbke
  197. poPMVdbvhYh6N0aGeS38jRKgyN10KXmhDTAQDwseVFavBWAjVfx3DEwjtK2Z2GbA
  198. aL8JvAwRtqiPFkDMIKPL4UwxtXFws8SpMt6juroUkNyf6+BxNWYqmwXHPy8zCJAb
  199. xkxIJMlEc+s7qQsP3fILOo8Xn+dVzJ5sa5AoARoXm1GMjsdqaKAzq99Dic/dHnaQ
  200. Civev1PQsdwlYW2C2wNXNeIrxMndbDMFfNuZ6BnGHWJ/wjcp/pFs4YkyyZN8JH7L
  201. hP2FO4Jgham3AuP13kC3Ivea7V6hR8QNcDZRwFPOMIX4tXwQv1T72+7DZGaA25O7
  202. nQVXBGBjI0ABDADJMBYIcG0Yil9YxFs7aYzNbd7alUAr89VbY8eIGPHP3INFPM1w
  203. lBQCu+4j6xdEbhMpppLBZ9A5TEylP4C6qLtPa+oLtPeuSw8gHDE10XE4lbgPs376
  204. rL60XdImSOHhiduACUefYjqpcmFH9Bim1CC+koArYrSQJQx1Jri+OpnTaL/8UID0
  205. KzD/kEgMVGlHIVj9oJmb4+j9pW8I/g0wDSnIaEKFMxqu6SIVJ1GWj+MUMvZigjLC
  206. sNCZd7PnbOC5VeU3SsXj6he74Jx0AmGMPWIHi9M0DjHO5d1cCbXTnud8xxM1bOh4
  207. 7aCTnMK5cVyIr+adihgJpVVhrndSM8aklBPRgtozrGNCgF2CkYU2P1blxfloNr/8
  208. UZpM83o+s1aObBszzRNLxnpNORqoLqjfPtLEPQnagxE+4EapCq0NZ/x6yO5VTwwp
  209. NljdFAEk40uGuKyn1QA3uNMHy5DlpLl+tU7t1KEovdZ+OVYsYKZhVzw0MTpKogk9
  210. JI7AN0q62ronPskAEQEAAQAL+O8BUSt1ZCVjPSIXIsrR+ZOSkszZwgJ1CWIoh0IH
  211. YD2vmcMHGIhFYgBdgerpvhptKhaw7GcXDScEnYkyh5s4GE2hxclik1tbj/x1gYCN
  212. 8BNoyeDdPFxQG73qN12D99QYEctpOsz9xPLIDwmL0j1ehAfhwqHIAPm9Ca+i8JYM
  213. x/F+35S/jnKDXRI+NVlwbiEyXKXxxIqNlpy9i8sDBGexO5H5Sg0zSN/B1duLekGD
  214. biDw6gLc6bCgnS+0JOUpU07Z2fccMOY9ncjKGD2uIb/ePPUaek92GCQyq0eorCIV
  215. brcQsRc5sSsNtnRKQTQtxioROeDg7kf2oWySeHTswlXW/219ihrSXgteHJd+rPm7
  216. DYLEeGLRny8bRKv8rQdAtApHaJE4dAATXeY4RYo4NlXHYaztGYtU6kiM/3zCfWAe
  217. 9Nn+Wh9jMTZrjefUCagS5r6ZqAh7veNo/vgIGaCLh0a1Ypa0Yk9KFrn3LYEM3zgk
  218. 3m3bn+7qgy5cUYXoJ3DGJJEhBgDPonpW0WElqLs5ZMem1ha85SC38F0IkAaSuzuz
  219. v3eORiKWuyJGF32Q2XHa1RHQs1JtUKd8rxFer3b8Oq71zLz6JtVc9dmRudvgcJYX
  220. 0PC11F6WGjZFSSp39dajFp0A5DKUs39F3w7J1yuDM56TDIN810ywufGAHARY1pZb
  221. UJAy/dTqjFnCbNjpAakor3hVzqxcmUG+7Y2X9c2AGncT1MqAQC3M8JZcuZvkK8A9
  222. cMk8B914ryYE7VsZMdMhyTwHmykGAPgNLLa3RDETeGeGCKWI+ZPOoU0ib5JtJZ1d
  223. P3tNwfZKuZBZXKW9gqYqyBa/qhMip84SP30pr/TvulcdAFC759HK8sQZyJ6Vw24P
  224. c+5ssRxrQUEw1rvJPWhmQCmCOZHBMQl5T6eaTOpR5u3aUKTMlxPKhK9eC1dCSTnI
  225. /nyL8An3VKnLy+K/LI42YGphBVLLJmBewuTVDIJviWRdntiG8dElyEJMOywUltk3
  226. 2CEmqgsD9tPO8rXZjnMrMn3gfsiaoQYA6/6/e2utkHr7gAoWBgrBBdqVHsvqh5Ro
  227. 2DjLAOpZItO/EdCJfDAmbTYOa04535sBDP2tcH/vipPOPpbr1Y9Y/mNsKCulNxed
  228. yqAmEkKOcerLUP5UHju0AB6VBjHJFdU2mqT+UjPyBk7WeKXgFomyoYMv3KpNOFWR
  229. xi0Xji4kKHbttA6Hy3UcGPr9acyUAlDYeKmxbSUYIPhw32bbGrX9+F5YriTufRsG
  230. 3jftQVo9zqdcQSD/5pUTMn3EYbEcohYB2YWJAbYEGAEIACACGwwWIQRqkzk/UMXm
  231. rNPW+0W5NiEu204UwAUCYNwR6wAKCRC5NiEu204UwOPnC/92PgB1c3h9FBXH1maz
  232. g29fndHIHH65VLgqMiQ7HAMojwRlT5Xnj5tdkCBmszRkv5vMvdJRa3ZY8Ed/Inqr
  233. hxBFNzpjqX4oj/RYIQLKXWWfkTKYVLJFZFPCSo00jesw2gieu3Ke/Yy4gwhtNodA
  234. v+s6QNMvffTW/K3XNrWDB0E7/LXbdidzhm+MBu8ov2tuC3tp9liLICiE1jv/2xT4
  235. CNSO6yphmk1/1zEYHS/mN9qJ2csBmte2cdmGyOcuVEHk3pyINNMDOamaURBJGRwF
  236. XB5V7gTKUFU4jCp3chywKrBHJHxGGDUmPBmZtDtfWAOgL32drK7/KUyzZL/WO7Fj
  237. akOI0hRDFOcqTYWL20H7+hAiX3oHMP7eou3L5C7wJ9+JMcACklN/WMjG9a536DFJ
  238. 4UgZ6HyKPP+wy837Hbe8b25kNMBwFgiaLR0lcgzxj7NyQWjVCMOEN+M55tRCjvL6
  239. ya6JVZCRbMXfdCy8lVPgtNQ6VlHaj8Wvnn2FLbWWO2n2r3s=
  240. =9zU5
  241. -----END PGP PRIVATE KEY BLOCK-----
  242. """
  243. NON_DEFAULT_KEY_ID = "6A93393F50C5E6ACD3D6FB45B936212EDB4E14C0"
  244. def setUp(self) -> None:
  245. super().setUp()
  246. self.gpg_dir = os.path.join(self.test_dir, "gpg")
  247. os.mkdir(self.gpg_dir, mode=0o700)
  248. # Ignore errors when deleting GNUPGHOME, because of race conditions
  249. # (e.g. the gpg-agent socket having been deleted). See
  250. # https://github.com/jelmer/dulwich/issues/1000
  251. self.addCleanup(shutil.rmtree, self.gpg_dir, ignore_errors=True)
  252. self.overrideEnv("GNUPGHOME", self.gpg_dir)
  253. def import_default_key(self) -> None:
  254. subprocess.run(
  255. ["gpg", "--import"],
  256. stdout=subprocess.DEVNULL,
  257. stderr=subprocess.DEVNULL,
  258. input=PorcelainGpgTestCase.DEFAULT_KEY,
  259. text=True,
  260. )
  261. def import_non_default_key(self) -> None:
  262. subprocess.run(
  263. ["gpg", "--import"],
  264. stdout=subprocess.DEVNULL,
  265. stderr=subprocess.DEVNULL,
  266. input=PorcelainGpgTestCase.NON_DEFAULT_KEY,
  267. text=True,
  268. )
  269. class ArchiveTests(PorcelainTestCase):
  270. """Tests for the archive command."""
  271. def test_simple(self) -> None:
  272. c1, c2, c3 = build_commit_graph(
  273. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  274. )
  275. self.repo.refs[b"refs/heads/master"] = c3.id
  276. out = BytesIO()
  277. err = BytesIO()
  278. porcelain.archive(
  279. self.repo.path, b"refs/heads/master", outstream=out, errstream=err
  280. )
  281. self.assertEqual(b"", err.getvalue())
  282. tf = tarfile.TarFile(fileobj=out)
  283. self.addCleanup(tf.close)
  284. self.assertEqual([], tf.getnames())
  285. class UpdateServerInfoTests(PorcelainTestCase):
  286. def test_simple(self) -> None:
  287. c1, c2, c3 = build_commit_graph(
  288. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  289. )
  290. self.repo.refs[b"refs/heads/foo"] = c3.id
  291. porcelain.update_server_info(self.repo.path)
  292. self.assertTrue(
  293. os.path.exists(os.path.join(self.repo.controldir(), "info", "refs"))
  294. )
  295. class CommitTests(PorcelainTestCase):
  296. def test_custom_author(self) -> None:
  297. c1, c2, c3 = build_commit_graph(
  298. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  299. )
  300. self.repo.refs[b"refs/heads/foo"] = c3.id
  301. sha = porcelain.commit(
  302. self.repo.path,
  303. message=b"Some message",
  304. author=b"Joe <joe@example.com>",
  305. committer=b"Bob <bob@example.com>",
  306. )
  307. self.assertIsInstance(sha, bytes)
  308. self.assertEqual(len(sha), 40)
  309. def test_unicode(self) -> None:
  310. c1, c2, c3 = build_commit_graph(
  311. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  312. )
  313. self.repo.refs[b"refs/heads/foo"] = c3.id
  314. sha = porcelain.commit(
  315. self.repo.path,
  316. message="Some message",
  317. author="Joe <joe@example.com>",
  318. committer="Bob <bob@example.com>",
  319. )
  320. self.assertIsInstance(sha, bytes)
  321. self.assertEqual(len(sha), 40)
  322. def test_no_verify(self) -> None:
  323. if os.name != "posix":
  324. self.skipTest("shell hook tests requires POSIX shell")
  325. self.assertTrue(os.path.exists("/bin/sh"))
  326. hooks_dir = os.path.join(self.repo.controldir(), "hooks")
  327. os.makedirs(hooks_dir, exist_ok=True)
  328. self.addCleanup(shutil.rmtree, hooks_dir)
  329. c1, c2, c3 = build_commit_graph(
  330. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  331. )
  332. hook_fail = "#!/bin/sh\nexit 1"
  333. # hooks are executed in pre-commit, commit-msg order
  334. # test commit-msg failure first, then pre-commit failure, then
  335. # no_verify to skip both hooks
  336. commit_msg = os.path.join(hooks_dir, "commit-msg")
  337. with open(commit_msg, "w") as f:
  338. f.write(hook_fail)
  339. os.chmod(commit_msg, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  340. with self.assertRaises(CommitError):
  341. porcelain.commit(
  342. self.repo.path,
  343. message="Some message",
  344. author="Joe <joe@example.com>",
  345. committer="Bob <bob@example.com>",
  346. )
  347. pre_commit = os.path.join(hooks_dir, "pre-commit")
  348. with open(pre_commit, "w") as f:
  349. f.write(hook_fail)
  350. os.chmod(pre_commit, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
  351. with self.assertRaises(CommitError):
  352. porcelain.commit(
  353. self.repo.path,
  354. message="Some message",
  355. author="Joe <joe@example.com>",
  356. committer="Bob <bob@example.com>",
  357. )
  358. sha = porcelain.commit(
  359. self.repo.path,
  360. message="Some message",
  361. author="Joe <joe@example.com>",
  362. committer="Bob <bob@example.com>",
  363. no_verify=True,
  364. )
  365. self.assertIsInstance(sha, bytes)
  366. self.assertEqual(len(sha), 40)
  367. def test_timezone(self) -> None:
  368. c1, c2, c3 = build_commit_graph(
  369. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  370. )
  371. self.repo.refs[b"refs/heads/foo"] = c3.id
  372. sha = porcelain.commit(
  373. self.repo.path,
  374. message="Some message",
  375. author="Joe <joe@example.com>",
  376. author_timezone=18000,
  377. committer="Bob <bob@example.com>",
  378. commit_timezone=18000,
  379. )
  380. self.assertIsInstance(sha, bytes)
  381. self.assertEqual(len(sha), 40)
  382. commit = self.repo.get_object(sha)
  383. self.assertEqual(commit._author_timezone, 18000)
  384. self.assertEqual(commit._commit_timezone, 18000)
  385. self.overrideEnv("GIT_AUTHOR_DATE", "1995-11-20T19:12:08-0501")
  386. self.overrideEnv("GIT_COMMITTER_DATE", "1995-11-20T19:12:08-0501")
  387. sha = porcelain.commit(
  388. self.repo.path,
  389. message="Some message",
  390. author="Joe <joe@example.com>",
  391. committer="Bob <bob@example.com>",
  392. )
  393. self.assertIsInstance(sha, bytes)
  394. self.assertEqual(len(sha), 40)
  395. commit = self.repo.get_object(sha)
  396. self.assertEqual(commit._author_timezone, -18060)
  397. self.assertEqual(commit._commit_timezone, -18060)
  398. self.overrideEnv("GIT_AUTHOR_DATE", None)
  399. self.overrideEnv("GIT_COMMITTER_DATE", None)
  400. local_timezone = time.localtime().tm_gmtoff
  401. sha = porcelain.commit(
  402. self.repo.path,
  403. message="Some message",
  404. author="Joe <joe@example.com>",
  405. committer="Bob <bob@example.com>",
  406. )
  407. self.assertIsInstance(sha, bytes)
  408. self.assertEqual(len(sha), 40)
  409. commit = self.repo.get_object(sha)
  410. self.assertEqual(commit._author_timezone, local_timezone)
  411. self.assertEqual(commit._commit_timezone, local_timezone)
  412. @skipIf(
  413. platform.python_implementation() == "PyPy" or sys.platform == "win32",
  414. "gpgme not easily available or supported on Windows and PyPy",
  415. )
  416. class CommitSignTests(PorcelainGpgTestCase):
  417. def test_default_key(self) -> None:
  418. c1, c2, c3 = build_commit_graph(
  419. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  420. )
  421. self.repo.refs[b"HEAD"] = c3.id
  422. cfg = self.repo.get_config()
  423. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  424. self.import_default_key()
  425. sha = porcelain.commit(
  426. self.repo.path,
  427. message="Some message",
  428. author="Joe <joe@example.com>",
  429. committer="Bob <bob@example.com>",
  430. signoff=True,
  431. )
  432. self.assertIsInstance(sha, bytes)
  433. self.assertEqual(len(sha), 40)
  434. commit = self.repo.get_object(sha)
  435. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  436. commit.verify()
  437. commit.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
  438. self.import_non_default_key()
  439. self.assertRaises(
  440. gpg.errors.MissingSignatures,
  441. commit.verify,
  442. keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
  443. )
  444. commit.committer = b"Alice <alice@example.com>"
  445. self.assertRaises(
  446. gpg.errors.BadSignatures,
  447. commit.verify,
  448. )
  449. def test_non_default_key(self) -> None:
  450. c1, c2, c3 = build_commit_graph(
  451. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  452. )
  453. self.repo.refs[b"HEAD"] = c3.id
  454. cfg = self.repo.get_config()
  455. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  456. self.import_non_default_key()
  457. sha = porcelain.commit(
  458. self.repo.path,
  459. message="Some message",
  460. author="Joe <joe@example.com>",
  461. committer="Bob <bob@example.com>",
  462. signoff=PorcelainGpgTestCase.NON_DEFAULT_KEY_ID,
  463. )
  464. self.assertIsInstance(sha, bytes)
  465. self.assertEqual(len(sha), 40)
  466. commit = self.repo.get_object(sha)
  467. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  468. commit.verify()
  469. class TimezoneTests(PorcelainTestCase):
  470. def put_envs(self, value) -> None:
  471. self.overrideEnv("GIT_AUTHOR_DATE", value)
  472. self.overrideEnv("GIT_COMMITTER_DATE", value)
  473. def fallback(self, value) -> None:
  474. self.put_envs(value)
  475. self.assertRaises(porcelain.TimezoneFormatError, porcelain.get_user_timezones)
  476. def test_internal_format(self) -> None:
  477. self.put_envs("0 +0500")
  478. self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
  479. def test_rfc_2822(self) -> None:
  480. self.put_envs("Mon, 20 Nov 1995 19:12:08 -0500")
  481. self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
  482. self.put_envs("Mon, 20 Nov 1995 19:12:08")
  483. self.assertTupleEqual((0, 0), porcelain.get_user_timezones())
  484. def test_iso8601(self) -> None:
  485. self.put_envs("1995-11-20T19:12:08-0501")
  486. self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
  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-05:01")
  490. self.assertTupleEqual((-18060, -18060), porcelain.get_user_timezones())
  491. self.put_envs("1995-11-20 19:12:08-05")
  492. self.assertTupleEqual((-18000, -18000), porcelain.get_user_timezones())
  493. # https://github.com/git/git/blob/96b2d4fa927c5055adc5b1d08f10a5d7352e2989/t/t6300-for-each-ref.sh#L128
  494. self.put_envs("2006-07-03 17:18:44 +0200")
  495. self.assertTupleEqual((7200, 7200), porcelain.get_user_timezones())
  496. def test_missing_or_malformed(self) -> None:
  497. # TODO: add more here
  498. self.fallback("0 + 0500")
  499. self.fallback("a +0500")
  500. self.fallback("1995-11-20T19:12:08")
  501. self.fallback("1995-11-20T19:12:08-05:")
  502. self.fallback("1995.11.20")
  503. self.fallback("11/20/1995")
  504. self.fallback("20.11.1995")
  505. def test_different_envs(self) -> None:
  506. self.overrideEnv("GIT_AUTHOR_DATE", "0 +0500")
  507. self.overrideEnv("GIT_COMMITTER_DATE", "0 +0501")
  508. self.assertTupleEqual((18000, 18060), porcelain.get_user_timezones())
  509. def test_no_envs(self) -> None:
  510. local_timezone = time.localtime().tm_gmtoff
  511. self.put_envs("0 +0500")
  512. self.assertTupleEqual((18000, 18000), porcelain.get_user_timezones())
  513. self.overrideEnv("GIT_COMMITTER_DATE", None)
  514. self.assertTupleEqual((18000, local_timezone), porcelain.get_user_timezones())
  515. self.put_envs("0 +0500")
  516. self.overrideEnv("GIT_AUTHOR_DATE", None)
  517. self.assertTupleEqual((local_timezone, 18000), porcelain.get_user_timezones())
  518. self.put_envs("0 +0500")
  519. self.overrideEnv("GIT_AUTHOR_DATE", None)
  520. self.overrideEnv("GIT_COMMITTER_DATE", None)
  521. self.assertTupleEqual(
  522. (local_timezone, local_timezone), porcelain.get_user_timezones()
  523. )
  524. class CleanTests(PorcelainTestCase):
  525. def put_files(self, tracked, ignored, untracked, empty_dirs) -> None:
  526. """Put the described files in the wd."""
  527. all_files = tracked | ignored | untracked
  528. for file_path in all_files:
  529. abs_path = os.path.join(self.repo.path, file_path)
  530. # File may need to be written in a dir that doesn't exist yet, so
  531. # create the parent dir(s) as necessary
  532. parent_dir = os.path.dirname(abs_path)
  533. try:
  534. os.makedirs(parent_dir)
  535. except FileExistsError:
  536. pass
  537. with open(abs_path, "w") as f:
  538. f.write("")
  539. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  540. f.writelines(ignored)
  541. for dir_path in empty_dirs:
  542. os.mkdir(os.path.join(self.repo.path, "empty_dir"))
  543. files_to_add = [os.path.join(self.repo.path, t) for t in tracked]
  544. porcelain.add(repo=self.repo.path, paths=files_to_add)
  545. porcelain.commit(repo=self.repo.path, message="init commit")
  546. def assert_wd(self, expected_paths) -> None:
  547. """Assert paths of files and dirs in wd are same as expected_paths."""
  548. control_dir_rel = os.path.relpath(self.repo._controldir, self.repo.path)
  549. # normalize paths to simplify comparison across platforms
  550. found_paths = {
  551. os.path.normpath(p)
  552. for p in flat_walk_dir(self.repo.path)
  553. if not p.split(os.sep)[0] == control_dir_rel
  554. }
  555. norm_expected_paths = {os.path.normpath(p) for p in expected_paths}
  556. self.assertEqual(found_paths, norm_expected_paths)
  557. def test_from_root(self) -> None:
  558. self.put_files(
  559. tracked={"tracked_file", "tracked_dir/tracked_file", ".gitignore"},
  560. ignored={"ignored_file"},
  561. untracked={
  562. "untracked_file",
  563. "tracked_dir/untracked_dir/untracked_file",
  564. "untracked_dir/untracked_dir/untracked_file",
  565. },
  566. empty_dirs={"empty_dir"},
  567. )
  568. porcelain.clean(repo=self.repo.path, target_dir=self.repo.path)
  569. self.assert_wd(
  570. {
  571. "tracked_file",
  572. "tracked_dir/tracked_file",
  573. ".gitignore",
  574. "ignored_file",
  575. "tracked_dir",
  576. }
  577. )
  578. def test_from_subdir(self) -> None:
  579. self.put_files(
  580. tracked={"tracked_file", "tracked_dir/tracked_file", ".gitignore"},
  581. ignored={"ignored_file"},
  582. untracked={
  583. "untracked_file",
  584. "tracked_dir/untracked_dir/untracked_file",
  585. "untracked_dir/untracked_dir/untracked_file",
  586. },
  587. empty_dirs={"empty_dir"},
  588. )
  589. porcelain.clean(
  590. repo=self.repo,
  591. target_dir=os.path.join(self.repo.path, "untracked_dir"),
  592. )
  593. self.assert_wd(
  594. {
  595. "tracked_file",
  596. "tracked_dir/tracked_file",
  597. ".gitignore",
  598. "ignored_file",
  599. "untracked_file",
  600. "tracked_dir/untracked_dir/untracked_file",
  601. "empty_dir",
  602. "untracked_dir",
  603. "tracked_dir",
  604. "tracked_dir/untracked_dir",
  605. }
  606. )
  607. class CloneTests(PorcelainTestCase):
  608. def test_simple_local(self) -> None:
  609. f1_1 = make_object(Blob, data=b"f1")
  610. commit_spec = [[1], [2, 1], [3, 1, 2]]
  611. trees = {
  612. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  613. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  614. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  615. }
  616. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  617. self.repo.refs[b"refs/heads/master"] = c3.id
  618. self.repo.refs[b"refs/tags/foo"] = c3.id
  619. target_path = tempfile.mkdtemp()
  620. errstream = BytesIO()
  621. self.addCleanup(shutil.rmtree, target_path)
  622. r = porcelain.clone(
  623. self.repo.path, target_path, checkout=False, errstream=errstream
  624. )
  625. self.addCleanup(r.close)
  626. self.assertEqual(r.path, target_path)
  627. target_repo = Repo(target_path)
  628. self.assertEqual(0, len(target_repo.open_index()))
  629. self.assertEqual(c3.id, target_repo.refs[b"refs/tags/foo"])
  630. self.assertNotIn(b"f1", os.listdir(target_path))
  631. self.assertNotIn(b"f2", os.listdir(target_path))
  632. c = r.get_config()
  633. encoded_path = self.repo.path
  634. if not isinstance(encoded_path, bytes):
  635. encoded_path = encoded_path.encode("utf-8")
  636. self.assertEqual(encoded_path, c.get((b"remote", b"origin"), b"url"))
  637. self.assertEqual(
  638. b"+refs/heads/*:refs/remotes/origin/*",
  639. c.get((b"remote", b"origin"), b"fetch"),
  640. )
  641. def test_simple_local_with_checkout(self) -> None:
  642. f1_1 = make_object(Blob, data=b"f1")
  643. commit_spec = [[1], [2, 1], [3, 1, 2]]
  644. trees = {
  645. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  646. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  647. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  648. }
  649. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  650. self.repo.refs[b"refs/heads/master"] = c3.id
  651. target_path = tempfile.mkdtemp()
  652. errstream = BytesIO()
  653. self.addCleanup(shutil.rmtree, target_path)
  654. with porcelain.clone(
  655. self.repo.path, target_path, checkout=True, errstream=errstream
  656. ) as r:
  657. self.assertEqual(r.path, target_path)
  658. with Repo(target_path) as r:
  659. self.assertEqual(r.head(), c3.id)
  660. self.assertIn("f1", os.listdir(target_path))
  661. self.assertIn("f2", os.listdir(target_path))
  662. def test_bare_local_with_checkout(self) -> None:
  663. f1_1 = make_object(Blob, data=b"f1")
  664. commit_spec = [[1], [2, 1], [3, 1, 2]]
  665. trees = {
  666. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  667. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  668. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  669. }
  670. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  671. self.repo.refs[b"refs/heads/master"] = c3.id
  672. target_path = tempfile.mkdtemp()
  673. errstream = BytesIO()
  674. self.addCleanup(shutil.rmtree, target_path)
  675. with porcelain.clone(
  676. self.repo.path, target_path, bare=True, errstream=errstream
  677. ) as r:
  678. self.assertEqual(r.path, target_path)
  679. with Repo(target_path) as r:
  680. r.head()
  681. self.assertRaises(NoIndexPresent, r.open_index)
  682. self.assertNotIn(b"f1", os.listdir(target_path))
  683. self.assertNotIn(b"f2", os.listdir(target_path))
  684. def test_no_checkout_with_bare(self) -> None:
  685. f1_1 = make_object(Blob, data=b"f1")
  686. commit_spec = [[1]]
  687. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  688. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  689. self.repo.refs[b"refs/heads/master"] = c1.id
  690. self.repo.refs[b"HEAD"] = c1.id
  691. target_path = tempfile.mkdtemp()
  692. errstream = BytesIO()
  693. self.addCleanup(shutil.rmtree, target_path)
  694. self.assertRaises(
  695. porcelain.Error,
  696. porcelain.clone,
  697. self.repo.path,
  698. target_path,
  699. checkout=True,
  700. bare=True,
  701. errstream=errstream,
  702. )
  703. def test_no_head_no_checkout(self) -> None:
  704. f1_1 = make_object(Blob, data=b"f1")
  705. commit_spec = [[1]]
  706. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  707. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  708. self.repo.refs[b"refs/heads/master"] = c1.id
  709. target_path = tempfile.mkdtemp()
  710. self.addCleanup(shutil.rmtree, target_path)
  711. errstream = BytesIO()
  712. r = porcelain.clone(
  713. self.repo.path, target_path, checkout=True, errstream=errstream
  714. )
  715. r.close()
  716. def test_no_head_no_checkout_outstream_errstream_autofallback(self) -> None:
  717. f1_1 = make_object(Blob, data=b"f1")
  718. commit_spec = [[1]]
  719. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  720. (c1,) = build_commit_graph(self.repo.object_store, commit_spec, trees)
  721. self.repo.refs[b"refs/heads/master"] = c1.id
  722. target_path = tempfile.mkdtemp()
  723. self.addCleanup(shutil.rmtree, target_path)
  724. errstream = porcelain.NoneStream()
  725. r = porcelain.clone(
  726. self.repo.path, target_path, checkout=True, errstream=errstream
  727. )
  728. r.close()
  729. def test_source_broken(self) -> None:
  730. with tempfile.TemporaryDirectory() as parent:
  731. target_path = os.path.join(parent, "target")
  732. self.assertRaises(
  733. Exception, porcelain.clone, "/nonexistent/repo", target_path
  734. )
  735. self.assertFalse(os.path.exists(target_path))
  736. def test_fetch_symref(self) -> None:
  737. f1_1 = make_object(Blob, data=b"f1")
  738. trees = {1: [(b"f1", f1_1), (b"f2", f1_1)]}
  739. [c1] = build_commit_graph(self.repo.object_store, [[1]], trees)
  740. self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/else")
  741. self.repo.refs[b"refs/heads/else"] = c1.id
  742. target_path = tempfile.mkdtemp()
  743. errstream = BytesIO()
  744. self.addCleanup(shutil.rmtree, target_path)
  745. r = porcelain.clone(
  746. self.repo.path, target_path, checkout=False, errstream=errstream
  747. )
  748. self.addCleanup(r.close)
  749. self.assertEqual(r.path, target_path)
  750. target_repo = Repo(target_path)
  751. self.assertEqual(0, len(target_repo.open_index()))
  752. self.assertEqual(c1.id, target_repo.refs[b"refs/heads/else"])
  753. self.assertEqual(c1.id, target_repo.refs[b"HEAD"])
  754. self.assertEqual(
  755. {
  756. b"HEAD": b"refs/heads/else",
  757. b"refs/remotes/origin/HEAD": b"refs/remotes/origin/else",
  758. },
  759. target_repo.refs.get_symrefs(),
  760. )
  761. def test_detached_head(self) -> None:
  762. f1_1 = make_object(Blob, data=b"f1")
  763. commit_spec = [[1], [2, 1], [3, 1, 2]]
  764. trees = {
  765. 1: [(b"f1", f1_1), (b"f2", f1_1)],
  766. 2: [(b"f1", f1_1), (b"f2", f1_1)],
  767. 3: [(b"f1", f1_1), (b"f2", f1_1)],
  768. }
  769. c1, c2, c3 = build_commit_graph(self.repo.object_store, commit_spec, trees)
  770. self.repo.refs[b"refs/heads/master"] = c2.id
  771. self.repo.refs.remove_if_equals(b"HEAD", None)
  772. self.repo.refs[b"HEAD"] = c3.id
  773. target_path = tempfile.mkdtemp()
  774. self.addCleanup(shutil.rmtree, target_path)
  775. errstream = porcelain.NoneStream()
  776. with porcelain.clone(
  777. self.repo.path, target_path, checkout=True, errstream=errstream
  778. ) as r:
  779. self.assertEqual(c3.id, r.refs[b"HEAD"])
  780. class InitTests(TestCase):
  781. def test_non_bare(self) -> None:
  782. repo_dir = tempfile.mkdtemp()
  783. self.addCleanup(shutil.rmtree, repo_dir)
  784. porcelain.init(repo_dir)
  785. def test_bare(self) -> None:
  786. repo_dir = tempfile.mkdtemp()
  787. self.addCleanup(shutil.rmtree, repo_dir)
  788. porcelain.init(repo_dir, bare=True)
  789. class AddTests(PorcelainTestCase):
  790. def test_add_default_paths(self) -> None:
  791. # create a file for initial commit
  792. fullpath = os.path.join(self.repo.path, "blah")
  793. with open(fullpath, "w") as f:
  794. f.write("\n")
  795. porcelain.add(repo=self.repo.path, paths=[fullpath])
  796. porcelain.commit(
  797. repo=self.repo.path,
  798. message=b"test",
  799. author=b"test <email>",
  800. committer=b"test <email>",
  801. )
  802. # Add a second test file and a file in a directory
  803. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  804. f.write("\n")
  805. os.mkdir(os.path.join(self.repo.path, "adir"))
  806. with open(os.path.join(self.repo.path, "adir", "afile"), "w") as f:
  807. f.write("\n")
  808. cwd = os.getcwd()
  809. try:
  810. os.chdir(self.repo.path)
  811. self.assertEqual({"foo", "blah", "adir", ".git"}, set(os.listdir(".")))
  812. self.assertEqual(
  813. (["foo", os.path.join("adir", "afile")], set()),
  814. porcelain.add(self.repo.path),
  815. )
  816. finally:
  817. os.chdir(cwd)
  818. # Check that foo was added and nothing in .git was modified
  819. index = self.repo.open_index()
  820. self.assertEqual(sorted(index), [b"adir/afile", b"blah", b"foo"])
  821. def test_add_default_paths_subdir(self) -> None:
  822. os.mkdir(os.path.join(self.repo.path, "foo"))
  823. with open(os.path.join(self.repo.path, "blah"), "w") as f:
  824. f.write("\n")
  825. with open(os.path.join(self.repo.path, "foo", "blie"), "w") as f:
  826. f.write("\n")
  827. cwd = os.getcwd()
  828. try:
  829. os.chdir(os.path.join(self.repo.path, "foo"))
  830. porcelain.add(repo=self.repo.path)
  831. porcelain.commit(
  832. repo=self.repo.path,
  833. message=b"test",
  834. author=b"test <email>",
  835. committer=b"test <email>",
  836. )
  837. finally:
  838. os.chdir(cwd)
  839. index = self.repo.open_index()
  840. self.assertEqual(sorted(index), [b"foo/blie"])
  841. def test_add_file(self) -> None:
  842. fullpath = os.path.join(self.repo.path, "foo")
  843. with open(fullpath, "w") as f:
  844. f.write("BAR")
  845. porcelain.add(self.repo.path, paths=[fullpath])
  846. self.assertIn(b"foo", self.repo.open_index())
  847. def test_add_ignored(self) -> None:
  848. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  849. f.write("foo\nsubdir/")
  850. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  851. f.write("BAR")
  852. with open(os.path.join(self.repo.path, "bar"), "w") as f:
  853. f.write("BAR")
  854. os.mkdir(os.path.join(self.repo.path, "subdir"))
  855. with open(os.path.join(self.repo.path, "subdir", "baz"), "w") as f:
  856. f.write("BAZ")
  857. (added, ignored) = porcelain.add(
  858. self.repo.path,
  859. paths=[
  860. os.path.join(self.repo.path, "foo"),
  861. os.path.join(self.repo.path, "bar"),
  862. os.path.join(self.repo.path, "subdir"),
  863. ],
  864. )
  865. self.assertIn(b"bar", self.repo.open_index())
  866. self.assertEqual({"bar"}, set(added))
  867. self.assertEqual({"foo", os.path.join("subdir", "")}, ignored)
  868. def test_add_file_absolute_path(self) -> None:
  869. # Absolute paths are (not yet) supported
  870. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  871. f.write("BAR")
  872. porcelain.add(self.repo, paths=[os.path.join(self.repo.path, "foo")])
  873. self.assertIn(b"foo", self.repo.open_index())
  874. def test_add_not_in_repo(self) -> None:
  875. with open(os.path.join(self.test_dir, "foo"), "w") as f:
  876. f.write("BAR")
  877. self.assertRaises(
  878. ValueError,
  879. porcelain.add,
  880. self.repo,
  881. paths=[os.path.join(self.test_dir, "foo")],
  882. )
  883. self.assertRaises(
  884. (ValueError, FileNotFoundError),
  885. porcelain.add,
  886. self.repo,
  887. paths=["../foo"],
  888. )
  889. self.assertEqual([], list(self.repo.open_index()))
  890. def test_add_file_clrf_conversion(self) -> None:
  891. # Set the right configuration to the repo
  892. c = self.repo.get_config()
  893. c.set("core", "autocrlf", "input")
  894. c.write_to_path()
  895. # Add a file with CRLF line-ending
  896. fullpath = os.path.join(self.repo.path, "foo")
  897. with open(fullpath, "wb") as f:
  898. f.write(b"line1\r\nline2")
  899. porcelain.add(self.repo.path, paths=[fullpath])
  900. # The line-endings should have been converted to LF
  901. index = self.repo.open_index()
  902. self.assertIn(b"foo", index)
  903. entry = index[b"foo"]
  904. blob = self.repo[entry.sha]
  905. self.assertEqual(blob.data, b"line1\nline2")
  906. class RemoveTests(PorcelainTestCase):
  907. def test_remove_file(self) -> None:
  908. fullpath = os.path.join(self.repo.path, "foo")
  909. with open(fullpath, "w") as f:
  910. f.write("BAR")
  911. porcelain.add(self.repo.path, paths=[fullpath])
  912. porcelain.commit(
  913. repo=self.repo,
  914. message=b"test",
  915. author=b"test <email>",
  916. committer=b"test <email>",
  917. )
  918. self.assertTrue(os.path.exists(os.path.join(self.repo.path, "foo")))
  919. cwd = os.getcwd()
  920. try:
  921. os.chdir(self.repo.path)
  922. porcelain.remove(self.repo.path, paths=["foo"])
  923. finally:
  924. os.chdir(cwd)
  925. self.assertFalse(os.path.exists(os.path.join(self.repo.path, "foo")))
  926. def test_remove_file_staged(self) -> None:
  927. fullpath = os.path.join(self.repo.path, "foo")
  928. with open(fullpath, "w") as f:
  929. f.write("BAR")
  930. cwd = os.getcwd()
  931. try:
  932. os.chdir(self.repo.path)
  933. porcelain.add(self.repo.path, paths=[fullpath])
  934. self.assertRaises(Exception, porcelain.rm, self.repo.path, paths=["foo"])
  935. finally:
  936. os.chdir(cwd)
  937. def test_remove_file_removed_on_disk(self) -> None:
  938. fullpath = os.path.join(self.repo.path, "foo")
  939. with open(fullpath, "w") as f:
  940. f.write("BAR")
  941. porcelain.add(self.repo.path, paths=[fullpath])
  942. cwd = os.getcwd()
  943. try:
  944. os.chdir(self.repo.path)
  945. os.remove(fullpath)
  946. porcelain.remove(self.repo.path, paths=["foo"])
  947. finally:
  948. os.chdir(cwd)
  949. self.assertFalse(os.path.exists(os.path.join(self.repo.path, "foo")))
  950. class LogTests(PorcelainTestCase):
  951. def test_simple(self) -> None:
  952. c1, c2, c3 = build_commit_graph(
  953. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  954. )
  955. self.repo.refs[b"HEAD"] = c3.id
  956. self.maxDiff = None
  957. outstream = StringIO()
  958. porcelain.log(self.repo.path, outstream=outstream)
  959. self.assertEqual(
  960. outstream.getvalue(),
  961. """\
  962. --------------------------------------------------
  963. commit: 4a3b887baa9ecb2d054d2469b628aef84e2d74f0
  964. merge: 7508036b1cfec5aa9cef0d5a7f04abcecfe09112
  965. Author: Test Author <test@nodomain.com>
  966. Committer: Test Committer <test@nodomain.com>
  967. Date: Fri Jan 01 2010 00:00:00 +0000
  968. Commit 3
  969. --------------------------------------------------
  970. commit: 7508036b1cfec5aa9cef0d5a7f04abcecfe09112
  971. Author: Test Author <test@nodomain.com>
  972. Committer: Test Committer <test@nodomain.com>
  973. Date: Fri Jan 01 2010 00:00:00 +0000
  974. Commit 2
  975. --------------------------------------------------
  976. commit: 11d3cf672a19366435c1983c7340b008ec6b8bf3
  977. Author: Test Author <test@nodomain.com>
  978. Committer: Test Committer <test@nodomain.com>
  979. Date: Fri Jan 01 2010 00:00:00 +0000
  980. Commit 1
  981. """,
  982. )
  983. def test_max_entries(self) -> None:
  984. c1, c2, c3 = build_commit_graph(
  985. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  986. )
  987. self.repo.refs[b"HEAD"] = c3.id
  988. outstream = StringIO()
  989. porcelain.log(self.repo.path, outstream=outstream, max_entries=1)
  990. self.assertEqual(1, outstream.getvalue().count("-" * 50))
  991. def test_no_revisions(self) -> None:
  992. outstream = StringIO()
  993. porcelain.log(self.repo.path, outstream=outstream)
  994. self.assertEqual("", outstream.getvalue())
  995. def test_empty_message(self) -> None:
  996. c1 = make_commit(message="")
  997. self.repo.object_store.add_object(c1)
  998. self.repo.refs[b"HEAD"] = c1.id
  999. outstream = StringIO()
  1000. porcelain.log(self.repo.path, outstream=outstream)
  1001. self.assertEqual(
  1002. outstream.getvalue(),
  1003. """\
  1004. --------------------------------------------------
  1005. commit: 4a7ad5552fad70647a81fb9a4a923ccefcca4b76
  1006. Author: Test Author <test@nodomain.com>
  1007. Committer: Test Committer <test@nodomain.com>
  1008. Date: Fri Jan 01 2010 00:00:00 +0000
  1009. """,
  1010. )
  1011. class ShowTests(PorcelainTestCase):
  1012. def test_nolist(self) -> None:
  1013. c1, c2, c3 = build_commit_graph(
  1014. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1015. )
  1016. self.repo.refs[b"HEAD"] = c3.id
  1017. outstream = StringIO()
  1018. porcelain.show(self.repo.path, objects=c3.id, outstream=outstream)
  1019. self.assertTrue(outstream.getvalue().startswith("-" * 50))
  1020. def test_simple(self) -> None:
  1021. c1, c2, c3 = build_commit_graph(
  1022. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1023. )
  1024. self.repo.refs[b"HEAD"] = c3.id
  1025. outstream = StringIO()
  1026. porcelain.show(self.repo.path, objects=[c3.id], outstream=outstream)
  1027. self.assertTrue(outstream.getvalue().startswith("-" * 50))
  1028. def test_blob(self) -> None:
  1029. b = Blob.from_string(b"The Foo\n")
  1030. self.repo.object_store.add_object(b)
  1031. outstream = StringIO()
  1032. porcelain.show(self.repo.path, objects=[b.id], outstream=outstream)
  1033. self.assertEqual(outstream.getvalue(), "The Foo\n")
  1034. def test_commit_no_parent(self) -> None:
  1035. a = Blob.from_string(b"The Foo\n")
  1036. ta = Tree()
  1037. ta.add(b"somename", 0o100644, a.id)
  1038. ca = make_commit(tree=ta.id)
  1039. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1040. outstream = StringIO()
  1041. porcelain.show(self.repo.path, objects=[ca.id], outstream=outstream)
  1042. self.assertMultiLineEqual(
  1043. outstream.getvalue(),
  1044. """\
  1045. --------------------------------------------------
  1046. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1047. Author: Test Author <test@nodomain.com>
  1048. Committer: Test Committer <test@nodomain.com>
  1049. Date: Fri Jan 01 2010 00:00:00 +0000
  1050. Test message.
  1051. diff --git a/somename b/somename
  1052. new file mode 100644
  1053. index 0000000..ea5c7bf
  1054. --- /dev/null
  1055. +++ b/somename
  1056. @@ -0,0 +1 @@
  1057. +The Foo
  1058. """,
  1059. )
  1060. def test_tag(self) -> None:
  1061. a = Blob.from_string(b"The Foo\n")
  1062. ta = Tree()
  1063. ta.add(b"somename", 0o100644, a.id)
  1064. ca = make_commit(tree=ta.id)
  1065. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1066. porcelain.tag_create(
  1067. self.repo.path,
  1068. b"tryme",
  1069. b"foo <foo@bar.com>",
  1070. b"bar",
  1071. annotated=True,
  1072. objectish=ca.id,
  1073. tag_time=1552854211,
  1074. tag_timezone=0,
  1075. )
  1076. outstream = StringIO()
  1077. porcelain.show(self.repo, objects=[b"refs/tags/tryme"], outstream=outstream)
  1078. self.maxDiff = None
  1079. self.assertMultiLineEqual(
  1080. outstream.getvalue(),
  1081. """\
  1082. Tagger: foo <foo@bar.com>
  1083. Date: Sun Mar 17 2019 20:23:31 +0000
  1084. bar
  1085. --------------------------------------------------
  1086. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1087. Author: Test Author <test@nodomain.com>
  1088. Committer: Test Committer <test@nodomain.com>
  1089. Date: Fri Jan 01 2010 00:00:00 +0000
  1090. Test message.
  1091. diff --git a/somename b/somename
  1092. new file mode 100644
  1093. index 0000000..ea5c7bf
  1094. --- /dev/null
  1095. +++ b/somename
  1096. @@ -0,0 +1 @@
  1097. +The Foo
  1098. """,
  1099. )
  1100. def test_tag_unicode(self) -> None:
  1101. a = Blob.from_string(b"The Foo\n")
  1102. ta = Tree()
  1103. ta.add(b"somename", 0o100644, a.id)
  1104. ca = make_commit(tree=ta.id)
  1105. self.repo.object_store.add_objects([(a, None), (ta, None), (ca, None)])
  1106. porcelain.tag_create(
  1107. self.repo.path,
  1108. "tryme",
  1109. "foo <foo@bar.com>",
  1110. "bar",
  1111. annotated=True,
  1112. objectish=ca.id,
  1113. tag_time=1552854211,
  1114. tag_timezone=0,
  1115. )
  1116. outstream = StringIO()
  1117. porcelain.show(self.repo, objects=[b"refs/tags/tryme"], outstream=outstream)
  1118. self.maxDiff = None
  1119. self.assertMultiLineEqual(
  1120. outstream.getvalue(),
  1121. """\
  1122. Tagger: foo <foo@bar.com>
  1123. Date: Sun Mar 17 2019 20:23:31 +0000
  1124. bar
  1125. --------------------------------------------------
  1126. commit: 344da06c1bb85901270b3e8875c988a027ec087d
  1127. Author: Test Author <test@nodomain.com>
  1128. Committer: Test Committer <test@nodomain.com>
  1129. Date: Fri Jan 01 2010 00:00:00 +0000
  1130. Test message.
  1131. diff --git a/somename b/somename
  1132. new file mode 100644
  1133. index 0000000..ea5c7bf
  1134. --- /dev/null
  1135. +++ b/somename
  1136. @@ -0,0 +1 @@
  1137. +The Foo
  1138. """,
  1139. )
  1140. def test_commit_with_change(self) -> None:
  1141. a = Blob.from_string(b"The Foo\n")
  1142. ta = Tree()
  1143. ta.add(b"somename", 0o100644, a.id)
  1144. ca = make_commit(tree=ta.id)
  1145. b = Blob.from_string(b"The Bar\n")
  1146. tb = Tree()
  1147. tb.add(b"somename", 0o100644, b.id)
  1148. cb = make_commit(tree=tb.id, parents=[ca.id])
  1149. self.repo.object_store.add_objects(
  1150. [
  1151. (a, None),
  1152. (b, None),
  1153. (ta, None),
  1154. (tb, None),
  1155. (ca, None),
  1156. (cb, None),
  1157. ]
  1158. )
  1159. outstream = StringIO()
  1160. porcelain.show(self.repo.path, objects=[cb.id], outstream=outstream)
  1161. self.assertMultiLineEqual(
  1162. outstream.getvalue(),
  1163. """\
  1164. --------------------------------------------------
  1165. commit: 2c6b6c9cb72c130956657e1fdae58e5b103744fa
  1166. Author: Test Author <test@nodomain.com>
  1167. Committer: Test Committer <test@nodomain.com>
  1168. Date: Fri Jan 01 2010 00:00:00 +0000
  1169. Test message.
  1170. diff --git a/somename b/somename
  1171. index ea5c7bf..fd38bcb 100644
  1172. --- a/somename
  1173. +++ b/somename
  1174. @@ -1 +1 @@
  1175. -The Foo
  1176. +The Bar
  1177. """,
  1178. )
  1179. class SymbolicRefTests(PorcelainTestCase):
  1180. def test_set_wrong_symbolic_ref(self) -> None:
  1181. c1, c2, c3 = build_commit_graph(
  1182. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1183. )
  1184. self.repo.refs[b"HEAD"] = c3.id
  1185. self.assertRaises(
  1186. porcelain.Error, porcelain.symbolic_ref, self.repo.path, b"foobar"
  1187. )
  1188. def test_set_force_wrong_symbolic_ref(self) -> None:
  1189. c1, c2, c3 = build_commit_graph(
  1190. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1191. )
  1192. self.repo.refs[b"HEAD"] = c3.id
  1193. porcelain.symbolic_ref(self.repo.path, b"force_foobar", force=True)
  1194. # test if we actually changed the file
  1195. with self.repo.get_named_file("HEAD") as f:
  1196. new_ref = f.read()
  1197. self.assertEqual(new_ref, b"ref: refs/heads/force_foobar\n")
  1198. def test_set_symbolic_ref(self) -> None:
  1199. c1, c2, c3 = build_commit_graph(
  1200. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1201. )
  1202. self.repo.refs[b"HEAD"] = c3.id
  1203. porcelain.symbolic_ref(self.repo.path, b"master")
  1204. def test_set_symbolic_ref_other_than_master(self) -> None:
  1205. c1, c2, c3 = build_commit_graph(
  1206. self.repo.object_store,
  1207. [[1], [2, 1], [3, 1, 2]],
  1208. attrs=dict(refs="develop"),
  1209. )
  1210. self.repo.refs[b"HEAD"] = c3.id
  1211. self.repo.refs[b"refs/heads/develop"] = c3.id
  1212. porcelain.symbolic_ref(self.repo.path, b"develop")
  1213. # test if we actually changed the file
  1214. with self.repo.get_named_file("HEAD") as f:
  1215. new_ref = f.read()
  1216. self.assertEqual(new_ref, b"ref: refs/heads/develop\n")
  1217. class DiffTreeTests(PorcelainTestCase):
  1218. def test_empty(self) -> None:
  1219. c1, c2, c3 = build_commit_graph(
  1220. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1221. )
  1222. self.repo.refs[b"HEAD"] = c3.id
  1223. outstream = BytesIO()
  1224. porcelain.diff_tree(self.repo.path, c2.tree, c3.tree, outstream=outstream)
  1225. self.assertEqual(outstream.getvalue(), b"")
  1226. class CommitTreeTests(PorcelainTestCase):
  1227. def test_simple(self) -> None:
  1228. c1, c2, c3 = build_commit_graph(
  1229. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1230. )
  1231. b = Blob()
  1232. b.data = b"foo the bar"
  1233. t = Tree()
  1234. t.add(b"somename", 0o100644, b.id)
  1235. self.repo.object_store.add_object(t)
  1236. self.repo.object_store.add_object(b)
  1237. sha = porcelain.commit_tree(
  1238. self.repo.path,
  1239. t.id,
  1240. message=b"Withcommit.",
  1241. author=b"Joe <joe@example.com>",
  1242. committer=b"Jane <jane@example.com>",
  1243. )
  1244. self.assertIsInstance(sha, bytes)
  1245. self.assertEqual(len(sha), 40)
  1246. class RevListTests(PorcelainTestCase):
  1247. def test_simple(self) -> None:
  1248. c1, c2, c3 = build_commit_graph(
  1249. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1250. )
  1251. outstream = BytesIO()
  1252. porcelain.rev_list(self.repo.path, [c3.id], outstream=outstream)
  1253. self.assertEqual(
  1254. c3.id + b"\n" + c2.id + b"\n" + c1.id + b"\n", outstream.getvalue()
  1255. )
  1256. @skipIf(
  1257. platform.python_implementation() == "PyPy" or sys.platform == "win32",
  1258. "gpgme not easily available or supported on Windows and PyPy",
  1259. )
  1260. class TagCreateSignTests(PorcelainGpgTestCase):
  1261. def test_default_key(self) -> None:
  1262. c1, c2, c3 = build_commit_graph(
  1263. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1264. )
  1265. self.repo.refs[b"HEAD"] = c3.id
  1266. cfg = self.repo.get_config()
  1267. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  1268. self.import_default_key()
  1269. porcelain.tag_create(
  1270. self.repo.path,
  1271. b"tryme",
  1272. b"foo <foo@bar.com>",
  1273. b"bar",
  1274. annotated=True,
  1275. sign=True,
  1276. )
  1277. tags = self.repo.refs.as_dict(b"refs/tags")
  1278. self.assertEqual(list(tags.keys()), [b"tryme"])
  1279. tag = self.repo[b"refs/tags/tryme"]
  1280. self.assertIsInstance(tag, Tag)
  1281. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1282. self.assertEqual(b"bar\n", tag.message)
  1283. self.assertRecentTimestamp(tag.tag_time)
  1284. tag = self.repo[b"refs/tags/tryme"]
  1285. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  1286. tag.verify()
  1287. tag.verify(keyids=[PorcelainGpgTestCase.DEFAULT_KEY_ID])
  1288. self.import_non_default_key()
  1289. self.assertRaises(
  1290. gpg.errors.MissingSignatures,
  1291. tag.verify,
  1292. keyids=[PorcelainGpgTestCase.NON_DEFAULT_KEY_ID],
  1293. )
  1294. tag._chunked_text = [b"bad data", tag._signature]
  1295. self.assertRaises(
  1296. gpg.errors.BadSignatures,
  1297. tag.verify,
  1298. )
  1299. def test_non_default_key(self) -> None:
  1300. c1, c2, c3 = build_commit_graph(
  1301. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1302. )
  1303. self.repo.refs[b"HEAD"] = c3.id
  1304. cfg = self.repo.get_config()
  1305. cfg.set(("user",), "signingKey", PorcelainGpgTestCase.DEFAULT_KEY_ID)
  1306. self.import_non_default_key()
  1307. porcelain.tag_create(
  1308. self.repo.path,
  1309. b"tryme",
  1310. b"foo <foo@bar.com>",
  1311. b"bar",
  1312. annotated=True,
  1313. sign=PorcelainGpgTestCase.NON_DEFAULT_KEY_ID,
  1314. )
  1315. tags = self.repo.refs.as_dict(b"refs/tags")
  1316. self.assertEqual(list(tags.keys()), [b"tryme"])
  1317. tag = self.repo[b"refs/tags/tryme"]
  1318. self.assertIsInstance(tag, Tag)
  1319. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1320. self.assertEqual(b"bar\n", tag.message)
  1321. self.assertRecentTimestamp(tag.tag_time)
  1322. tag = self.repo[b"refs/tags/tryme"]
  1323. # GPG Signatures aren't deterministic, so we can't do a static assertion.
  1324. tag.verify()
  1325. class TagCreateTests(PorcelainTestCase):
  1326. def test_annotated(self) -> None:
  1327. c1, c2, c3 = build_commit_graph(
  1328. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1329. )
  1330. self.repo.refs[b"HEAD"] = c3.id
  1331. porcelain.tag_create(
  1332. self.repo.path,
  1333. b"tryme",
  1334. b"foo <foo@bar.com>",
  1335. b"bar",
  1336. annotated=True,
  1337. )
  1338. tags = self.repo.refs.as_dict(b"refs/tags")
  1339. self.assertEqual(list(tags.keys()), [b"tryme"])
  1340. tag = self.repo[b"refs/tags/tryme"]
  1341. self.assertIsInstance(tag, Tag)
  1342. self.assertEqual(b"foo <foo@bar.com>", tag.tagger)
  1343. self.assertEqual(b"bar\n", tag.message)
  1344. self.assertRecentTimestamp(tag.tag_time)
  1345. def test_unannotated(self) -> None:
  1346. c1, c2, c3 = build_commit_graph(
  1347. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1348. )
  1349. self.repo.refs[b"HEAD"] = c3.id
  1350. porcelain.tag_create(self.repo.path, b"tryme", annotated=False)
  1351. tags = self.repo.refs.as_dict(b"refs/tags")
  1352. self.assertEqual(list(tags.keys()), [b"tryme"])
  1353. self.repo[b"refs/tags/tryme"]
  1354. self.assertEqual(list(tags.values()), [self.repo.head()])
  1355. def test_unannotated_unicode(self) -> None:
  1356. c1, c2, c3 = build_commit_graph(
  1357. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  1358. )
  1359. self.repo.refs[b"HEAD"] = c3.id
  1360. porcelain.tag_create(self.repo.path, "tryme", annotated=False)
  1361. tags = self.repo.refs.as_dict(b"refs/tags")
  1362. self.assertEqual(list(tags.keys()), [b"tryme"])
  1363. self.repo[b"refs/tags/tryme"]
  1364. self.assertEqual(list(tags.values()), [self.repo.head()])
  1365. class TagListTests(PorcelainTestCase):
  1366. def test_empty(self) -> None:
  1367. tags = porcelain.tag_list(self.repo.path)
  1368. self.assertEqual([], tags)
  1369. def test_simple(self) -> None:
  1370. self.repo.refs[b"refs/tags/foo"] = b"aa" * 20
  1371. self.repo.refs[b"refs/tags/bar/bla"] = b"bb" * 20
  1372. tags = porcelain.tag_list(self.repo.path)
  1373. self.assertEqual([b"bar/bla", b"foo"], tags)
  1374. class TagDeleteTests(PorcelainTestCase):
  1375. def test_simple(self) -> None:
  1376. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  1377. self.repo[b"HEAD"] = c1.id
  1378. porcelain.tag_create(self.repo, b"foo")
  1379. self.assertIn(b"foo", porcelain.tag_list(self.repo))
  1380. porcelain.tag_delete(self.repo, b"foo")
  1381. self.assertNotIn(b"foo", porcelain.tag_list(self.repo))
  1382. class ResetTests(PorcelainTestCase):
  1383. def test_hard_head(self) -> None:
  1384. fullpath = os.path.join(self.repo.path, "foo")
  1385. with open(fullpath, "w") as f:
  1386. f.write("BAR")
  1387. porcelain.add(self.repo.path, paths=[fullpath])
  1388. porcelain.commit(
  1389. self.repo.path,
  1390. message=b"Some message",
  1391. committer=b"Jane <jane@example.com>",
  1392. author=b"John <john@example.com>",
  1393. )
  1394. with open(os.path.join(self.repo.path, "foo"), "wb") as f:
  1395. f.write(b"OOH")
  1396. porcelain.reset(self.repo, "hard", b"HEAD")
  1397. index = self.repo.open_index()
  1398. changes = list(
  1399. tree_changes(
  1400. self.repo,
  1401. index.commit(self.repo.object_store),
  1402. self.repo[b"HEAD"].tree,
  1403. )
  1404. )
  1405. self.assertEqual([], changes)
  1406. def test_hard_commit(self) -> None:
  1407. fullpath = os.path.join(self.repo.path, "foo")
  1408. with open(fullpath, "w") as f:
  1409. f.write("BAR")
  1410. porcelain.add(self.repo.path, paths=[fullpath])
  1411. sha = porcelain.commit(
  1412. self.repo.path,
  1413. message=b"Some message",
  1414. committer=b"Jane <jane@example.com>",
  1415. author=b"John <john@example.com>",
  1416. )
  1417. with open(fullpath, "wb") as f:
  1418. f.write(b"BAZ")
  1419. porcelain.add(self.repo.path, paths=[fullpath])
  1420. porcelain.commit(
  1421. self.repo.path,
  1422. message=b"Some other message",
  1423. committer=b"Jane <jane@example.com>",
  1424. author=b"John <john@example.com>",
  1425. )
  1426. porcelain.reset(self.repo, "hard", sha)
  1427. index = self.repo.open_index()
  1428. changes = list(
  1429. tree_changes(
  1430. self.repo,
  1431. index.commit(self.repo.object_store),
  1432. self.repo[sha].tree,
  1433. )
  1434. )
  1435. self.assertEqual([], changes)
  1436. class ResetFileTests(PorcelainTestCase):
  1437. def test_reset_modify_file_to_commit(self) -> None:
  1438. file = "foo"
  1439. full_path = os.path.join(self.repo.path, file)
  1440. with open(full_path, "w") as f:
  1441. f.write("hello")
  1442. porcelain.add(self.repo, paths=[full_path])
  1443. sha = porcelain.commit(
  1444. self.repo,
  1445. message=b"unitest",
  1446. committer=b"Jane <jane@example.com>",
  1447. author=b"John <john@example.com>",
  1448. )
  1449. with open(full_path, "a") as f:
  1450. f.write("something new")
  1451. porcelain.reset_file(self.repo, file, target=sha)
  1452. with open(full_path) as f:
  1453. self.assertEqual("hello", f.read())
  1454. def test_reset_remove_file_to_commit(self) -> None:
  1455. file = "foo"
  1456. full_path = os.path.join(self.repo.path, file)
  1457. with open(full_path, "w") as f:
  1458. f.write("hello")
  1459. porcelain.add(self.repo, paths=[full_path])
  1460. sha = porcelain.commit(
  1461. self.repo,
  1462. message=b"unitest",
  1463. committer=b"Jane <jane@example.com>",
  1464. author=b"John <john@example.com>",
  1465. )
  1466. os.remove(full_path)
  1467. porcelain.reset_file(self.repo, file, target=sha)
  1468. with open(full_path) as f:
  1469. self.assertEqual("hello", f.read())
  1470. def test_resetfile_with_dir(self) -> None:
  1471. os.mkdir(os.path.join(self.repo.path, "new_dir"))
  1472. full_path = os.path.join(self.repo.path, "new_dir", "foo")
  1473. with open(full_path, "w") as f:
  1474. f.write("hello")
  1475. porcelain.add(self.repo, paths=[full_path])
  1476. sha = porcelain.commit(
  1477. self.repo,
  1478. message=b"unitest",
  1479. committer=b"Jane <jane@example.com>",
  1480. author=b"John <john@example.com>",
  1481. )
  1482. with open(full_path, "a") as f:
  1483. f.write("something new")
  1484. porcelain.commit(
  1485. self.repo,
  1486. message=b"unitest 2",
  1487. committer=b"Jane <jane@example.com>",
  1488. author=b"John <john@example.com>",
  1489. )
  1490. porcelain.reset_file(self.repo, os.path.join("new_dir", "foo"), target=sha)
  1491. with open(full_path) as f:
  1492. self.assertEqual("hello", f.read())
  1493. def _commit_file_with_content(repo, filename, content):
  1494. file_path = os.path.join(repo.path, filename)
  1495. with open(file_path, "w") as f:
  1496. f.write(content)
  1497. porcelain.add(repo, paths=[file_path])
  1498. sha = porcelain.commit(
  1499. repo,
  1500. message=b"add " + filename.encode(),
  1501. committer=b"Jane <jane@example.com>",
  1502. author=b"John <john@example.com>",
  1503. )
  1504. return sha, file_path
  1505. class CheckoutTests(PorcelainTestCase):
  1506. def setUp(self) -> None:
  1507. super().setUp()
  1508. self._sha, self._foo_path = _commit_file_with_content(
  1509. self.repo, "foo", "hello\n"
  1510. )
  1511. porcelain.branch_create(self.repo, "uni")
  1512. def test_checkout_to_existing_branch(self) -> None:
  1513. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1514. porcelain.checkout_branch(self.repo, b"uni")
  1515. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1516. def test_checkout_to_non_existing_branch(self) -> None:
  1517. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1518. with self.assertRaises(KeyError):
  1519. porcelain.checkout_branch(self.repo, b"bob")
  1520. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1521. def test_checkout_to_branch_with_modified_files(self) -> None:
  1522. with open(self._foo_path, "a") as f:
  1523. f.write("new message\n")
  1524. porcelain.add(self.repo, paths=[self._foo_path])
  1525. status = list(porcelain.status(self.repo))
  1526. self.assertEqual(
  1527. [{"add": [], "delete": [], "modify": [b"foo"]}, [], []], status
  1528. )
  1529. # The new checkout behavior prevents switching with staged changes
  1530. with self.assertRaises(porcelain.CheckoutError):
  1531. porcelain.checkout_branch(self.repo, b"uni")
  1532. # Should still be on master
  1533. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1534. # Force checkout should work
  1535. porcelain.checkout_branch(self.repo, b"uni", force=True)
  1536. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1537. def test_checkout_with_deleted_files(self) -> None:
  1538. porcelain.remove(self.repo.path, [os.path.join(self.repo.path, "foo")])
  1539. status = list(porcelain.status(self.repo))
  1540. self.assertEqual(
  1541. [{"add": [], "delete": [b"foo"], "modify": []}, [], []], status
  1542. )
  1543. # The new checkout behavior prevents switching with staged deletions
  1544. with self.assertRaises(porcelain.CheckoutError):
  1545. porcelain.checkout_branch(self.repo, b"uni")
  1546. # Should still be on master
  1547. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1548. # Force checkout should work
  1549. porcelain.checkout_branch(self.repo, b"uni", force=True)
  1550. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1551. def test_checkout_to_branch_with_added_files(self) -> None:
  1552. file_path = os.path.join(self.repo.path, "bar")
  1553. with open(file_path, "w") as f:
  1554. f.write("bar content\n")
  1555. porcelain.add(self.repo, paths=[file_path])
  1556. status = list(porcelain.status(self.repo))
  1557. self.assertEqual(
  1558. [{"add": [b"bar"], "delete": [], "modify": []}, [], []], status
  1559. )
  1560. # Both branches have file 'foo' checkout should be fine.
  1561. porcelain.checkout_branch(self.repo, b"uni")
  1562. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1563. status = list(porcelain.status(self.repo))
  1564. self.assertEqual(
  1565. [{"add": [b"bar"], "delete": [], "modify": []}, [], []], status
  1566. )
  1567. def test_checkout_to_branch_with_modified_file_not_present(self) -> None:
  1568. # Commit a new file that the other branch doesn't have.
  1569. _, nee_path = _commit_file_with_content(self.repo, "nee", "Good content\n")
  1570. # Modify the file the other branch doesn't have.
  1571. with open(nee_path, "a") as f:
  1572. f.write("bar content\n")
  1573. porcelain.add(self.repo, paths=[nee_path])
  1574. status = list(porcelain.status(self.repo))
  1575. self.assertEqual(
  1576. [{"add": [], "delete": [], "modify": [b"nee"]}, [], []], status
  1577. )
  1578. # The new checkout behavior allows switching if the file doesn't exist in target branch
  1579. # (changes can be preserved)
  1580. porcelain.checkout_branch(self.repo, b"uni")
  1581. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1582. # The staged changes are lost and the file is removed from working tree
  1583. # because it doesn't exist in the target branch
  1584. status = list(porcelain.status(self.repo))
  1585. # File 'nee' is gone completely
  1586. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1587. self.assertFalse(os.path.exists(nee_path))
  1588. def test_checkout_to_branch_with_modified_file_not_present_forced(self) -> None:
  1589. # Commit a new file that the other branch doesn't have.
  1590. _, nee_path = _commit_file_with_content(self.repo, "nee", "Good content\n")
  1591. # Modify the file the other branch doesn't have.
  1592. with open(nee_path, "a") as f:
  1593. f.write("bar content\n")
  1594. porcelain.add(self.repo, paths=[nee_path])
  1595. status = list(porcelain.status(self.repo))
  1596. self.assertEqual(
  1597. [{"add": [], "delete": [], "modify": [b"nee"]}, [], []], status
  1598. )
  1599. # 'uni' branch doesn't have 'nee' and it has been modified, but we force to reset the entire index.
  1600. porcelain.checkout_branch(self.repo, b"uni", force=True)
  1601. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1602. status = list(porcelain.status(self.repo))
  1603. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1604. def test_checkout_to_branch_with_unstaged_files(self) -> None:
  1605. # Edit `foo`.
  1606. with open(self._foo_path, "a") as f:
  1607. f.write("new message")
  1608. status = list(porcelain.status(self.repo))
  1609. self.assertEqual(
  1610. [{"add": [], "delete": [], "modify": []}, [b"foo"], []], status
  1611. )
  1612. # The new checkout behavior prevents switching with unstaged changes
  1613. with self.assertRaises(porcelain.CheckoutError):
  1614. porcelain.checkout_branch(self.repo, b"uni")
  1615. # Should still be on master
  1616. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1617. # Force checkout should work
  1618. porcelain.checkout_branch(self.repo, b"uni", force=True)
  1619. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1620. def test_checkout_to_branch_with_untracked_files(self) -> None:
  1621. with open(os.path.join(self.repo.path, "neu"), "a") as f:
  1622. f.write("new message\n")
  1623. status = list(porcelain.status(self.repo))
  1624. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], ["neu"]], status)
  1625. porcelain.checkout_branch(self.repo, b"uni")
  1626. status = list(porcelain.status(self.repo))
  1627. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], ["neu"]], status)
  1628. def test_checkout_to_branch_with_new_files(self) -> None:
  1629. porcelain.checkout_branch(self.repo, b"uni")
  1630. sub_directory = os.path.join(self.repo.path, "sub1")
  1631. os.mkdir(sub_directory)
  1632. for index in range(5):
  1633. _commit_file_with_content(
  1634. self.repo, "new_file_" + str(index + 1), "Some content\n"
  1635. )
  1636. _commit_file_with_content(
  1637. self.repo,
  1638. os.path.join("sub1", "new_file_" + str(index + 10)),
  1639. "Good content\n",
  1640. )
  1641. status = list(porcelain.status(self.repo))
  1642. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1643. porcelain.checkout_branch(self.repo, b"master")
  1644. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1645. status = list(porcelain.status(self.repo))
  1646. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1647. porcelain.checkout_branch(self.repo, b"uni")
  1648. self.assertEqual(b"uni", porcelain.active_branch(self.repo))
  1649. status = list(porcelain.status(self.repo))
  1650. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1651. def test_checkout_to_branch_with_file_in_sub_directory(self) -> None:
  1652. sub_directory = os.path.join(self.repo.path, "sub1", "sub2")
  1653. os.makedirs(sub_directory)
  1654. sub_directory_file = os.path.join(sub_directory, "neu")
  1655. with open(sub_directory_file, "w") as f:
  1656. f.write("new message\n")
  1657. porcelain.add(self.repo, paths=[sub_directory_file])
  1658. porcelain.commit(
  1659. self.repo,
  1660. message=b"add " + sub_directory_file.encode(),
  1661. committer=b"Jane <jane@example.com>",
  1662. author=b"John <john@example.com>",
  1663. )
  1664. status = list(porcelain.status(self.repo))
  1665. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1666. self.assertTrue(os.path.isdir(sub_directory))
  1667. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  1668. porcelain.checkout_branch(self.repo, b"uni")
  1669. status = list(porcelain.status(self.repo))
  1670. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1671. self.assertFalse(os.path.isdir(sub_directory))
  1672. self.assertFalse(os.path.isdir(os.path.dirname(sub_directory)))
  1673. porcelain.checkout_branch(self.repo, b"master")
  1674. self.assertTrue(os.path.isdir(sub_directory))
  1675. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  1676. def test_checkout_to_branch_with_multiple_files_in_sub_directory(self) -> None:
  1677. sub_directory = os.path.join(self.repo.path, "sub1", "sub2")
  1678. os.makedirs(sub_directory)
  1679. sub_directory_file_1 = os.path.join(sub_directory, "neu")
  1680. with open(sub_directory_file_1, "w") as f:
  1681. f.write("new message\n")
  1682. sub_directory_file_2 = os.path.join(sub_directory, "gus")
  1683. with open(sub_directory_file_2, "w") as f:
  1684. f.write("alternative message\n")
  1685. porcelain.add(self.repo, paths=[sub_directory_file_1, sub_directory_file_2])
  1686. porcelain.commit(
  1687. self.repo,
  1688. message=b"add files neu and gus.",
  1689. committer=b"Jane <jane@example.com>",
  1690. author=b"John <john@example.com>",
  1691. )
  1692. status = list(porcelain.status(self.repo))
  1693. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1694. self.assertTrue(os.path.isdir(sub_directory))
  1695. self.assertTrue(os.path.isdir(os.path.dirname(sub_directory)))
  1696. porcelain.checkout_branch(self.repo, b"uni")
  1697. status = list(porcelain.status(self.repo))
  1698. self.assertEqual([{"add": [], "delete": [], "modify": []}, [], []], status)
  1699. self.assertFalse(os.path.isdir(sub_directory))
  1700. self.assertFalse(os.path.isdir(os.path.dirname(sub_directory)))
  1701. def _commit_something_wrong(self):
  1702. with open(self._foo_path, "a") as f:
  1703. f.write("something wrong")
  1704. porcelain.add(self.repo, paths=[self._foo_path])
  1705. return porcelain.commit(
  1706. self.repo,
  1707. message=b"I may added something wrong",
  1708. committer=b"Jane <jane@example.com>",
  1709. author=b"John <john@example.com>",
  1710. )
  1711. def test_checkout_to_commit_sha(self) -> None:
  1712. self._commit_something_wrong()
  1713. porcelain.checkout_branch(self.repo, self._sha)
  1714. self.assertEqual(self._sha, self.repo.head())
  1715. def test_checkout_to_head(self) -> None:
  1716. new_sha = self._commit_something_wrong()
  1717. porcelain.checkout_branch(self.repo, b"HEAD")
  1718. self.assertEqual(new_sha, self.repo.head())
  1719. def _checkout_remote_branch(self):
  1720. errstream = BytesIO()
  1721. outstream = BytesIO()
  1722. porcelain.commit(
  1723. repo=self.repo.path,
  1724. message=b"init",
  1725. author=b"author <email>",
  1726. committer=b"committer <email>",
  1727. )
  1728. # Setup target repo cloned from temp test repo
  1729. clone_path = tempfile.mkdtemp()
  1730. self.addCleanup(shutil.rmtree, clone_path)
  1731. target_repo = porcelain.clone(
  1732. self.repo.path, target=clone_path, errstream=errstream
  1733. )
  1734. try:
  1735. self.assertEqual(target_repo[b"HEAD"], self.repo[b"HEAD"])
  1736. finally:
  1737. target_repo.close()
  1738. # create a second file to be pushed back to origin
  1739. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  1740. os.close(handle)
  1741. porcelain.add(repo=clone_path, paths=[fullpath])
  1742. porcelain.commit(
  1743. repo=clone_path,
  1744. message=b"push",
  1745. author=b"author <email>",
  1746. committer=b"committer <email>",
  1747. )
  1748. # Setup a non-checked out branch in the remote
  1749. refs_path = b"refs/heads/foo"
  1750. new_id = self.repo[b"HEAD"].id
  1751. self.assertNotEqual(new_id, ZERO_SHA)
  1752. self.repo.refs[refs_path] = new_id
  1753. # Push to the remote
  1754. porcelain.push(
  1755. clone_path,
  1756. "origin",
  1757. b"HEAD:" + refs_path,
  1758. outstream=outstream,
  1759. errstream=errstream,
  1760. )
  1761. self.assertEqual(
  1762. target_repo.refs[b"refs/remotes/origin/foo"],
  1763. target_repo.refs[b"HEAD"],
  1764. )
  1765. # The new checkout behavior treats origin/foo as a ref and creates detached HEAD
  1766. porcelain.checkout_branch(target_repo, b"origin/foo")
  1767. original_id = target_repo[b"HEAD"].id
  1768. uni_id = target_repo[b"refs/remotes/origin/uni"].id
  1769. # Should be in detached HEAD state
  1770. with self.assertRaises((ValueError, IndexError)):
  1771. porcelain.active_branch(target_repo)
  1772. expected_refs = {
  1773. b"HEAD": original_id,
  1774. b"refs/heads/master": original_id,
  1775. # No local foo branch is created anymore
  1776. b"refs/remotes/origin/foo": original_id,
  1777. b"refs/remotes/origin/uni": uni_id,
  1778. b"refs/remotes/origin/HEAD": new_id,
  1779. b"refs/remotes/origin/master": new_id,
  1780. }
  1781. self.assertEqual(expected_refs, target_repo.get_refs())
  1782. return target_repo
  1783. def test_checkout_remote_branch(self) -> None:
  1784. repo = self._checkout_remote_branch()
  1785. repo.close()
  1786. def test_checkout_remote_branch_then_master_then_remote_branch_again(self) -> None:
  1787. target_repo = self._checkout_remote_branch()
  1788. # Should be in detached HEAD state
  1789. with self.assertRaises((ValueError, IndexError)):
  1790. porcelain.active_branch(target_repo)
  1791. # Save the commit SHA before adding bar
  1792. detached_commit_sha, _ = _commit_file_with_content(
  1793. target_repo, "bar", "something\n"
  1794. )
  1795. self.assertTrue(os.path.isfile(os.path.join(target_repo.path, "bar")))
  1796. porcelain.checkout_branch(target_repo, b"master")
  1797. self.assertEqual(b"master", porcelain.active_branch(target_repo))
  1798. self.assertFalse(os.path.isfile(os.path.join(target_repo.path, "bar")))
  1799. # Going back to origin/foo won't have bar because the commit was made in detached state
  1800. porcelain.checkout_branch(target_repo, b"origin/foo")
  1801. # Should be in detached HEAD state again
  1802. with self.assertRaises((ValueError, IndexError)):
  1803. porcelain.active_branch(target_repo)
  1804. # bar is NOT there because we're back at the original origin/foo commit
  1805. self.assertFalse(os.path.isfile(os.path.join(target_repo.path, "bar")))
  1806. # But we can checkout the specific commit to get bar back
  1807. porcelain.checkout_branch(target_repo, detached_commit_sha.decode())
  1808. self.assertTrue(os.path.isfile(os.path.join(target_repo.path, "bar")))
  1809. target_repo.close()
  1810. class GeneralCheckoutTests(PorcelainTestCase):
  1811. """Tests for the general checkout function that handles branches, tags, and commits."""
  1812. def setUp(self) -> None:
  1813. super().setUp()
  1814. # Create initial commit
  1815. self._sha1, self._foo_path = _commit_file_with_content(
  1816. self.repo, "foo", "initial content\n"
  1817. )
  1818. # Create a branch
  1819. porcelain.branch_create(self.repo, "feature")
  1820. # Create another commit on master
  1821. self._sha2, self._bar_path = _commit_file_with_content(
  1822. self.repo, "bar", "bar content\n"
  1823. )
  1824. # Create a tag
  1825. porcelain.tag_create(self.repo, "v1.0", objectish=self._sha1)
  1826. def test_checkout_branch(self) -> None:
  1827. """Test checking out a branch."""
  1828. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1829. # Checkout feature branch
  1830. porcelain.checkout(self.repo, "feature")
  1831. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  1832. # File 'bar' should not exist in feature branch
  1833. self.assertFalse(os.path.exists(self._bar_path))
  1834. # Go back to master
  1835. porcelain.checkout(self.repo, "master")
  1836. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1837. # File 'bar' should exist again
  1838. self.assertTrue(os.path.exists(self._bar_path))
  1839. def test_checkout_commit(self) -> None:
  1840. """Test checking out a specific commit (detached HEAD)."""
  1841. # Checkout first commit by SHA
  1842. porcelain.checkout(self.repo, self._sha1.decode("ascii"))
  1843. # Should be in detached HEAD state - active_branch raises IndexError
  1844. with self.assertRaises((ValueError, IndexError)):
  1845. porcelain.active_branch(self.repo)
  1846. # File 'bar' should not exist
  1847. self.assertFalse(os.path.exists(self._bar_path))
  1848. # HEAD should point to the commit
  1849. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  1850. def test_checkout_tag(self) -> None:
  1851. """Test checking out a tag (detached HEAD)."""
  1852. # Checkout tag
  1853. porcelain.checkout(self.repo, "v1.0")
  1854. # Should be in detached HEAD state - active_branch raises IndexError
  1855. with self.assertRaises((ValueError, IndexError)):
  1856. porcelain.active_branch(self.repo)
  1857. # File 'bar' should not exist (tag points to first commit)
  1858. self.assertFalse(os.path.exists(self._bar_path))
  1859. # HEAD should point to the tagged commit
  1860. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  1861. def test_checkout_new_branch(self) -> None:
  1862. """Test creating a new branch during checkout (like git checkout -b)."""
  1863. # Create and checkout new branch from current HEAD
  1864. porcelain.checkout(self.repo, "master", new_branch="new-feature")
  1865. self.assertEqual(b"new-feature", porcelain.active_branch(self.repo))
  1866. self.assertTrue(os.path.exists(self._bar_path))
  1867. # Create and checkout new branch from specific commit
  1868. porcelain.checkout(self.repo, self._sha1.decode("ascii"), new_branch="from-old")
  1869. self.assertEqual(b"from-old", porcelain.active_branch(self.repo))
  1870. self.assertFalse(os.path.exists(self._bar_path))
  1871. def test_checkout_with_uncommitted_changes(self) -> None:
  1872. """Test checkout behavior with uncommitted changes."""
  1873. # Modify a file
  1874. with open(self._foo_path, "w") as f:
  1875. f.write("modified content\n")
  1876. # Should raise error when trying to checkout
  1877. with self.assertRaises(porcelain.CheckoutError) as cm:
  1878. porcelain.checkout(self.repo, "feature")
  1879. self.assertIn("local changes", str(cm.exception))
  1880. self.assertIn("foo", str(cm.exception))
  1881. # Should still be on master
  1882. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1883. def test_checkout_force(self) -> None:
  1884. """Test forced checkout discards local changes."""
  1885. # Modify a file
  1886. with open(self._foo_path, "w") as f:
  1887. f.write("modified content\n")
  1888. # Force checkout should succeed
  1889. porcelain.checkout(self.repo, "feature", force=True)
  1890. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  1891. # Local changes should be discarded
  1892. with open(self._foo_path) as f:
  1893. content = f.read()
  1894. self.assertEqual("initial content\n", content)
  1895. def test_checkout_nonexistent_ref(self) -> None:
  1896. """Test checkout of non-existent branch/commit."""
  1897. with self.assertRaises(KeyError):
  1898. porcelain.checkout(self.repo, "nonexistent")
  1899. def test_checkout_partial_sha(self) -> None:
  1900. """Test checkout with partial SHA."""
  1901. # Git typically allows checkout with partial SHA
  1902. partial_sha = self._sha1.decode("ascii")[:7]
  1903. porcelain.checkout(self.repo, partial_sha)
  1904. # Should be in detached HEAD state at the right commit
  1905. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  1906. def test_checkout_preserves_untracked_files(self) -> None:
  1907. """Test that checkout preserves untracked files."""
  1908. # Create an untracked file
  1909. untracked_path = os.path.join(self.repo.path, "untracked.txt")
  1910. with open(untracked_path, "w") as f:
  1911. f.write("untracked content\n")
  1912. # Checkout another branch
  1913. porcelain.checkout(self.repo, "feature")
  1914. # Untracked file should still exist
  1915. self.assertTrue(os.path.exists(untracked_path))
  1916. with open(untracked_path) as f:
  1917. content = f.read()
  1918. self.assertEqual("untracked content\n", content)
  1919. def test_checkout_full_ref_paths(self) -> None:
  1920. """Test checkout with full ref paths."""
  1921. # Test checkout with full branch ref path
  1922. porcelain.checkout(self.repo, "refs/heads/feature")
  1923. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  1924. # Test checkout with full tag ref path
  1925. porcelain.checkout(self.repo, "refs/tags/v1.0")
  1926. # Should be in detached HEAD state
  1927. with self.assertRaises((ValueError, IndexError)):
  1928. porcelain.active_branch(self.repo)
  1929. self.assertEqual(self._sha1, self.repo.refs[b"HEAD"])
  1930. def test_checkout_bytes_vs_string_target(self) -> None:
  1931. """Test that checkout works with both bytes and string targets."""
  1932. # Test with string target
  1933. porcelain.checkout(self.repo, "feature")
  1934. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  1935. # Test with bytes target
  1936. porcelain.checkout(self.repo, b"master")
  1937. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  1938. def test_checkout_new_branch_from_commit(self) -> None:
  1939. """Test creating a new branch from a specific commit."""
  1940. # Create new branch from first commit
  1941. porcelain.checkout(self.repo, self._sha1.decode(), new_branch="from-commit")
  1942. self.assertEqual(b"from-commit", porcelain.active_branch(self.repo))
  1943. # Should be at the first commit (no bar file)
  1944. self.assertFalse(os.path.exists(self._bar_path))
  1945. def test_checkout_with_staged_addition(self) -> None:
  1946. """Test checkout behavior with staged file additions."""
  1947. # Create and stage a new file that doesn't exist in target branch
  1948. new_file_path = os.path.join(self.repo.path, "new.txt")
  1949. with open(new_file_path, "w") as f:
  1950. f.write("new file content\n")
  1951. porcelain.add(self.repo, [new_file_path])
  1952. # This should succeed because the file doesn't exist in target branch
  1953. porcelain.checkout(self.repo, "feature")
  1954. # Should be on feature branch
  1955. self.assertEqual(b"feature", porcelain.active_branch(self.repo))
  1956. # The new file should still exist and be staged
  1957. self.assertTrue(os.path.exists(new_file_path))
  1958. status = porcelain.status(self.repo)
  1959. self.assertIn(b"new.txt", status.staged["add"])
  1960. def test_checkout_with_staged_modification_conflict(self) -> None:
  1961. """Test checkout behavior with staged modifications that would conflict."""
  1962. # Stage changes to a file that exists in both branches
  1963. with open(self._foo_path, "w") as f:
  1964. f.write("modified content\n")
  1965. porcelain.add(self.repo, [self._foo_path])
  1966. # Should prevent checkout due to staged changes to existing file
  1967. with self.assertRaises(porcelain.CheckoutError) as cm:
  1968. porcelain.checkout(self.repo, "feature")
  1969. self.assertIn("local changes", str(cm.exception))
  1970. self.assertIn("foo", str(cm.exception))
  1971. def test_checkout_head_reference(self) -> None:
  1972. """Test checkout of HEAD reference."""
  1973. # Move to feature branch first
  1974. porcelain.checkout(self.repo, "feature")
  1975. # Checkout HEAD creates detached HEAD state
  1976. porcelain.checkout(self.repo, "HEAD")
  1977. # Should be in detached HEAD state
  1978. with self.assertRaises((ValueError, IndexError)):
  1979. porcelain.active_branch(self.repo)
  1980. def test_checkout_error_messages(self) -> None:
  1981. """Test that checkout error messages are helpful."""
  1982. # Create uncommitted changes
  1983. with open(self._foo_path, "w") as f:
  1984. f.write("uncommitted changes\n")
  1985. # Try to checkout
  1986. with self.assertRaises(porcelain.CheckoutError) as cm:
  1987. porcelain.checkout(self.repo, "feature")
  1988. error_msg = str(cm.exception)
  1989. self.assertIn("local changes", error_msg)
  1990. self.assertIn("foo", error_msg)
  1991. self.assertIn("overwritten", error_msg)
  1992. self.assertIn("commit or stash", error_msg)
  1993. class SubmoduleTests(PorcelainTestCase):
  1994. def test_empty(self) -> None:
  1995. porcelain.commit(
  1996. repo=self.repo.path,
  1997. message=b"init",
  1998. author=b"author <email>",
  1999. committer=b"committer <email>",
  2000. )
  2001. self.assertEqual([], list(porcelain.submodule_list(self.repo)))
  2002. def test_add(self) -> None:
  2003. porcelain.submodule_add(self.repo, "../bar.git", "bar")
  2004. with open(f"{self.repo.path}/.gitmodules") as f:
  2005. self.assertEqual(
  2006. """\
  2007. [submodule "bar"]
  2008. \turl = ../bar.git
  2009. \tpath = bar
  2010. """,
  2011. f.read(),
  2012. )
  2013. def test_init(self) -> None:
  2014. porcelain.submodule_add(self.repo, "../bar.git", "bar")
  2015. porcelain.submodule_init(self.repo)
  2016. class PushTests(PorcelainTestCase):
  2017. def test_simple(self) -> None:
  2018. """Basic test of porcelain push where self.repo is the remote. First
  2019. clone the remote, commit a file to the clone, then push the changes
  2020. back to the remote.
  2021. """
  2022. outstream = BytesIO()
  2023. errstream = BytesIO()
  2024. porcelain.commit(
  2025. repo=self.repo.path,
  2026. message=b"init",
  2027. author=b"author <email>",
  2028. committer=b"committer <email>",
  2029. )
  2030. # Setup target repo cloned from temp test repo
  2031. clone_path = tempfile.mkdtemp()
  2032. self.addCleanup(shutil.rmtree, clone_path)
  2033. target_repo = porcelain.clone(
  2034. self.repo.path, target=clone_path, errstream=errstream
  2035. )
  2036. try:
  2037. self.assertEqual(target_repo[b"HEAD"], self.repo[b"HEAD"])
  2038. finally:
  2039. target_repo.close()
  2040. # create a second file to be pushed back to origin
  2041. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  2042. os.close(handle)
  2043. porcelain.add(repo=clone_path, paths=[fullpath])
  2044. porcelain.commit(
  2045. repo=clone_path,
  2046. message=b"push",
  2047. author=b"author <email>",
  2048. committer=b"committer <email>",
  2049. )
  2050. # Setup a non-checked out branch in the remote
  2051. refs_path = b"refs/heads/foo"
  2052. new_id = self.repo[b"HEAD"].id
  2053. self.assertNotEqual(new_id, ZERO_SHA)
  2054. self.repo.refs[refs_path] = new_id
  2055. # Push to the remote
  2056. porcelain.push(
  2057. clone_path,
  2058. "origin",
  2059. b"HEAD:" + refs_path,
  2060. outstream=outstream,
  2061. errstream=errstream,
  2062. )
  2063. self.assertEqual(
  2064. target_repo.refs[b"refs/remotes/origin/foo"],
  2065. target_repo.refs[b"HEAD"],
  2066. )
  2067. # Check that the target and source
  2068. with Repo(clone_path) as r_clone:
  2069. self.assertEqual(
  2070. {
  2071. b"HEAD": new_id,
  2072. b"refs/heads/foo": r_clone[b"HEAD"].id,
  2073. b"refs/heads/master": new_id,
  2074. },
  2075. self.repo.get_refs(),
  2076. )
  2077. self.assertEqual(r_clone[b"HEAD"].id, self.repo[refs_path].id)
  2078. # Get the change in the target repo corresponding to the add
  2079. # this will be in the foo branch.
  2080. change = next(
  2081. iter(
  2082. tree_changes(
  2083. self.repo,
  2084. self.repo[b"HEAD"].tree,
  2085. self.repo[b"refs/heads/foo"].tree,
  2086. )
  2087. )
  2088. )
  2089. self.assertEqual(
  2090. os.path.basename(fullpath), change.new.path.decode("ascii")
  2091. )
  2092. def test_local_missing(self) -> None:
  2093. """Pushing a new branch."""
  2094. outstream = BytesIO()
  2095. errstream = BytesIO()
  2096. # Setup target repo cloned from temp test repo
  2097. clone_path = tempfile.mkdtemp()
  2098. self.addCleanup(shutil.rmtree, clone_path)
  2099. target_repo = porcelain.init(clone_path)
  2100. target_repo.close()
  2101. self.assertRaises(
  2102. porcelain.Error,
  2103. porcelain.push,
  2104. self.repo,
  2105. clone_path,
  2106. b"HEAD:refs/heads/master",
  2107. outstream=outstream,
  2108. errstream=errstream,
  2109. )
  2110. def test_new(self) -> None:
  2111. """Pushing a new branch."""
  2112. outstream = BytesIO()
  2113. errstream = BytesIO()
  2114. # Setup target repo cloned from temp test repo
  2115. clone_path = tempfile.mkdtemp()
  2116. self.addCleanup(shutil.rmtree, clone_path)
  2117. target_repo = porcelain.init(clone_path)
  2118. target_repo.close()
  2119. # create a second file to be pushed back to origin
  2120. handle, fullpath = tempfile.mkstemp(dir=clone_path)
  2121. os.close(handle)
  2122. porcelain.add(repo=clone_path, paths=[fullpath])
  2123. new_id = porcelain.commit(
  2124. repo=self.repo,
  2125. message=b"push",
  2126. author=b"author <email>",
  2127. committer=b"committer <email>",
  2128. )
  2129. # Push to the remote
  2130. porcelain.push(
  2131. self.repo,
  2132. clone_path,
  2133. b"HEAD:refs/heads/master",
  2134. outstream=outstream,
  2135. errstream=errstream,
  2136. )
  2137. with Repo(clone_path) as r_clone:
  2138. self.assertEqual(
  2139. {
  2140. b"HEAD": new_id,
  2141. b"refs/heads/master": new_id,
  2142. },
  2143. r_clone.get_refs(),
  2144. )
  2145. def test_delete(self) -> None:
  2146. """Basic test of porcelain push, removing a branch."""
  2147. outstream = BytesIO()
  2148. errstream = BytesIO()
  2149. porcelain.commit(
  2150. repo=self.repo.path,
  2151. message=b"init",
  2152. author=b"author <email>",
  2153. committer=b"committer <email>",
  2154. )
  2155. # Setup target repo cloned from temp test repo
  2156. clone_path = tempfile.mkdtemp()
  2157. self.addCleanup(shutil.rmtree, clone_path)
  2158. target_repo = porcelain.clone(
  2159. self.repo.path, target=clone_path, errstream=errstream
  2160. )
  2161. target_repo.close()
  2162. # Setup a non-checked out branch in the remote
  2163. refs_path = b"refs/heads/foo"
  2164. new_id = self.repo[b"HEAD"].id
  2165. self.assertNotEqual(new_id, ZERO_SHA)
  2166. self.repo.refs[refs_path] = new_id
  2167. # Push to the remote
  2168. porcelain.push(
  2169. clone_path,
  2170. self.repo.path,
  2171. b":" + refs_path,
  2172. outstream=outstream,
  2173. errstream=errstream,
  2174. )
  2175. self.assertEqual(
  2176. {
  2177. b"HEAD": new_id,
  2178. b"refs/heads/master": new_id,
  2179. },
  2180. self.repo.get_refs(),
  2181. )
  2182. def test_diverged(self) -> None:
  2183. outstream = BytesIO()
  2184. errstream = BytesIO()
  2185. porcelain.commit(
  2186. repo=self.repo.path,
  2187. message=b"init",
  2188. author=b"author <email>",
  2189. committer=b"committer <email>",
  2190. )
  2191. # Setup target repo cloned from temp test repo
  2192. clone_path = tempfile.mkdtemp()
  2193. self.addCleanup(shutil.rmtree, clone_path)
  2194. target_repo = porcelain.clone(
  2195. self.repo.path, target=clone_path, errstream=errstream
  2196. )
  2197. target_repo.close()
  2198. remote_id = porcelain.commit(
  2199. repo=self.repo.path,
  2200. message=b"remote change",
  2201. author=b"author <email>",
  2202. committer=b"committer <email>",
  2203. )
  2204. local_id = porcelain.commit(
  2205. repo=clone_path,
  2206. message=b"local change",
  2207. author=b"author <email>",
  2208. committer=b"committer <email>",
  2209. )
  2210. outstream = BytesIO()
  2211. errstream = BytesIO()
  2212. # Push to the remote
  2213. self.assertRaises(
  2214. porcelain.DivergedBranches,
  2215. porcelain.push,
  2216. clone_path,
  2217. self.repo.path,
  2218. b"refs/heads/master",
  2219. outstream=outstream,
  2220. errstream=errstream,
  2221. )
  2222. self.assertEqual(
  2223. {
  2224. b"HEAD": remote_id,
  2225. b"refs/heads/master": remote_id,
  2226. },
  2227. self.repo.get_refs(),
  2228. )
  2229. self.assertEqual(b"", outstream.getvalue())
  2230. self.assertEqual(b"", errstream.getvalue())
  2231. outstream = BytesIO()
  2232. errstream = BytesIO()
  2233. # Push to the remote with --force
  2234. porcelain.push(
  2235. clone_path,
  2236. self.repo.path,
  2237. b"refs/heads/master",
  2238. outstream=outstream,
  2239. errstream=errstream,
  2240. force=True,
  2241. )
  2242. self.assertEqual(
  2243. {
  2244. b"HEAD": local_id,
  2245. b"refs/heads/master": local_id,
  2246. },
  2247. self.repo.get_refs(),
  2248. )
  2249. self.assertEqual(b"", outstream.getvalue())
  2250. self.assertTrue(re.match(b"Push to .* successful.\n", errstream.getvalue()))
  2251. class PullTests(PorcelainTestCase):
  2252. def setUp(self) -> None:
  2253. super().setUp()
  2254. # create a file for initial commit
  2255. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2256. os.close(handle)
  2257. porcelain.add(repo=self.repo.path, paths=fullpath)
  2258. porcelain.commit(
  2259. repo=self.repo.path,
  2260. message=b"test",
  2261. author=b"test <email>",
  2262. committer=b"test <email>",
  2263. )
  2264. # Setup target repo
  2265. self.target_path = tempfile.mkdtemp()
  2266. self.addCleanup(shutil.rmtree, self.target_path)
  2267. target_repo = porcelain.clone(
  2268. self.repo.path, target=self.target_path, errstream=BytesIO()
  2269. )
  2270. target_repo.close()
  2271. # create a second file to be pushed
  2272. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2273. os.close(handle)
  2274. porcelain.add(repo=self.repo.path, paths=fullpath)
  2275. porcelain.commit(
  2276. repo=self.repo.path,
  2277. message=b"test2",
  2278. author=b"test2 <email>",
  2279. committer=b"test2 <email>",
  2280. )
  2281. self.assertIn(b"refs/heads/master", self.repo.refs)
  2282. self.assertIn(b"refs/heads/master", target_repo.refs)
  2283. def test_simple(self) -> None:
  2284. outstream = BytesIO()
  2285. errstream = BytesIO()
  2286. # Pull changes into the cloned repo
  2287. porcelain.pull(
  2288. self.target_path,
  2289. self.repo.path,
  2290. b"refs/heads/master",
  2291. outstream=outstream,
  2292. errstream=errstream,
  2293. )
  2294. # Check the target repo for pushed changes
  2295. with Repo(self.target_path) as r:
  2296. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  2297. def test_diverged(self) -> None:
  2298. outstream = BytesIO()
  2299. errstream = BytesIO()
  2300. c3a = porcelain.commit(
  2301. repo=self.target_path,
  2302. message=b"test3a",
  2303. author=b"test2 <email>",
  2304. committer=b"test2 <email>",
  2305. )
  2306. porcelain.commit(
  2307. repo=self.repo.path,
  2308. message=b"test3b",
  2309. author=b"test2 <email>",
  2310. committer=b"test2 <email>",
  2311. )
  2312. # Pull changes into the cloned repo
  2313. self.assertRaises(
  2314. porcelain.DivergedBranches,
  2315. porcelain.pull,
  2316. self.target_path,
  2317. self.repo.path,
  2318. b"refs/heads/master",
  2319. outstream=outstream,
  2320. errstream=errstream,
  2321. )
  2322. # Check the target repo for pushed changes
  2323. with Repo(self.target_path) as r:
  2324. self.assertEqual(r[b"refs/heads/master"].id, c3a)
  2325. # Pull with merge should now work
  2326. porcelain.pull(
  2327. self.target_path,
  2328. self.repo.path,
  2329. b"refs/heads/master",
  2330. outstream=outstream,
  2331. errstream=errstream,
  2332. fast_forward=False,
  2333. )
  2334. # Check the target repo for merged changes
  2335. with Repo(self.target_path) as r:
  2336. # HEAD should now be a merge commit
  2337. head = r[b"HEAD"]
  2338. # It should have two parents
  2339. self.assertEqual(len(head.parents), 2)
  2340. # One parent should be the previous HEAD (c3a)
  2341. self.assertIn(c3a, head.parents)
  2342. # The other parent should be from the source repo
  2343. self.assertIn(self.repo[b"HEAD"].id, head.parents)
  2344. def test_no_refspec(self) -> None:
  2345. outstream = BytesIO()
  2346. errstream = BytesIO()
  2347. # Pull changes into the cloned repo
  2348. porcelain.pull(
  2349. self.target_path,
  2350. self.repo.path,
  2351. outstream=outstream,
  2352. errstream=errstream,
  2353. )
  2354. # Check the target repo for pushed changes
  2355. with Repo(self.target_path) as r:
  2356. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  2357. def test_no_remote_location(self) -> None:
  2358. outstream = BytesIO()
  2359. errstream = BytesIO()
  2360. # Pull changes into the cloned repo
  2361. porcelain.pull(
  2362. self.target_path,
  2363. refspecs=b"refs/heads/master",
  2364. outstream=outstream,
  2365. errstream=errstream,
  2366. )
  2367. # Check the target repo for pushed changes
  2368. with Repo(self.target_path) as r:
  2369. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  2370. def test_pull_updates_working_tree(self) -> None:
  2371. """Test that pull updates the working tree with new files."""
  2372. outstream = BytesIO()
  2373. errstream = BytesIO()
  2374. # Create a new file with content in the source repo
  2375. new_file = os.path.join(self.repo.path, "newfile.txt")
  2376. with open(new_file, "w") as f:
  2377. f.write("This is new content")
  2378. porcelain.add(repo=self.repo.path, paths=[new_file])
  2379. porcelain.commit(
  2380. repo=self.repo.path,
  2381. message=b"Add new file",
  2382. author=b"test <email>",
  2383. committer=b"test <email>",
  2384. )
  2385. # Before pull, the file should not exist in target
  2386. target_file = os.path.join(self.target_path, "newfile.txt")
  2387. self.assertFalse(os.path.exists(target_file))
  2388. # Pull changes into the cloned repo
  2389. porcelain.pull(
  2390. self.target_path,
  2391. self.repo.path,
  2392. b"refs/heads/master",
  2393. outstream=outstream,
  2394. errstream=errstream,
  2395. )
  2396. # After pull, the file should exist with correct content
  2397. self.assertTrue(os.path.exists(target_file))
  2398. with open(target_file) as f:
  2399. self.assertEqual(f.read(), "This is new content")
  2400. # Check the HEAD is updated too
  2401. with Repo(self.target_path) as r:
  2402. self.assertEqual(r[b"HEAD"].id, self.repo[b"HEAD"].id)
  2403. class StatusTests(PorcelainTestCase):
  2404. def test_empty(self) -> None:
  2405. results = porcelain.status(self.repo)
  2406. self.assertEqual({"add": [], "delete": [], "modify": []}, results.staged)
  2407. self.assertEqual([], results.unstaged)
  2408. def test_status_base(self) -> None:
  2409. """Integration test for `status` functionality."""
  2410. # Commit a dummy file then modify it
  2411. fullpath = os.path.join(self.repo.path, "foo")
  2412. with open(fullpath, "w") as f:
  2413. f.write("origstuff")
  2414. porcelain.add(repo=self.repo.path, paths=[fullpath])
  2415. porcelain.commit(
  2416. repo=self.repo.path,
  2417. message=b"test status",
  2418. author=b"author <email>",
  2419. committer=b"committer <email>",
  2420. )
  2421. # modify access and modify time of path
  2422. os.utime(fullpath, (0, 0))
  2423. with open(fullpath, "wb") as f:
  2424. f.write(b"stuff")
  2425. # Make a dummy file and stage it
  2426. filename_add = "bar"
  2427. fullpath = os.path.join(self.repo.path, filename_add)
  2428. with open(fullpath, "w") as f:
  2429. f.write("stuff")
  2430. porcelain.add(repo=self.repo.path, paths=fullpath)
  2431. results = porcelain.status(self.repo)
  2432. self.assertEqual(results.staged["add"][0], filename_add.encode("ascii"))
  2433. self.assertEqual(results.unstaged, [b"foo"])
  2434. def test_status_all(self) -> None:
  2435. del_path = os.path.join(self.repo.path, "foo")
  2436. mod_path = os.path.join(self.repo.path, "bar")
  2437. add_path = os.path.join(self.repo.path, "baz")
  2438. us_path = os.path.join(self.repo.path, "blye")
  2439. ut_path = os.path.join(self.repo.path, "blyat")
  2440. with open(del_path, "w") as f:
  2441. f.write("origstuff")
  2442. with open(mod_path, "w") as f:
  2443. f.write("origstuff")
  2444. with open(us_path, "w") as f:
  2445. f.write("origstuff")
  2446. porcelain.add(repo=self.repo.path, paths=[del_path, mod_path, us_path])
  2447. porcelain.commit(
  2448. repo=self.repo.path,
  2449. message=b"test status",
  2450. author=b"author <email>",
  2451. committer=b"committer <email>",
  2452. )
  2453. porcelain.remove(self.repo.path, [del_path])
  2454. with open(add_path, "w") as f:
  2455. f.write("origstuff")
  2456. with open(mod_path, "w") as f:
  2457. f.write("more_origstuff")
  2458. with open(us_path, "w") as f:
  2459. f.write("more_origstuff")
  2460. porcelain.add(repo=self.repo.path, paths=[add_path, mod_path])
  2461. with open(us_path, "w") as f:
  2462. f.write("\norigstuff")
  2463. with open(ut_path, "w") as f:
  2464. f.write("origstuff")
  2465. results = porcelain.status(self.repo.path)
  2466. self.assertDictEqual(
  2467. {"add": [b"baz"], "delete": [b"foo"], "modify": [b"bar"]},
  2468. results.staged,
  2469. )
  2470. self.assertListEqual(results.unstaged, [b"blye"])
  2471. results_no_untracked = porcelain.status(self.repo.path, untracked_files="no")
  2472. self.assertListEqual(results_no_untracked.untracked, [])
  2473. def test_status_wrong_untracked_files_value(self) -> None:
  2474. with self.assertRaises(ValueError):
  2475. porcelain.status(self.repo.path, untracked_files="antani")
  2476. def test_status_untracked_path(self) -> None:
  2477. untracked_dir = os.path.join(self.repo_path, "untracked_dir")
  2478. os.mkdir(untracked_dir)
  2479. untracked_file = os.path.join(untracked_dir, "untracked_file")
  2480. with open(untracked_file, "w") as fh:
  2481. fh.write("untracked")
  2482. _, _, untracked = porcelain.status(self.repo.path, untracked_files="all")
  2483. self.assertEqual(untracked, ["untracked_dir/untracked_file"])
  2484. def test_status_crlf_mismatch(self) -> None:
  2485. # First make a commit as if the file has been added on a Linux system
  2486. # or with core.autocrlf=True
  2487. file_path = os.path.join(self.repo.path, "crlf")
  2488. with open(file_path, "wb") as f:
  2489. f.write(b"line1\nline2")
  2490. porcelain.add(repo=self.repo.path, paths=[file_path])
  2491. porcelain.commit(
  2492. repo=self.repo.path,
  2493. message=b"test status",
  2494. author=b"author <email>",
  2495. committer=b"committer <email>",
  2496. )
  2497. # Then update the file as if it was created by CGit on a Windows
  2498. # system with core.autocrlf=true
  2499. with open(file_path, "wb") as f:
  2500. f.write(b"line1\r\nline2")
  2501. results = porcelain.status(self.repo)
  2502. self.assertDictEqual({"add": [], "delete": [], "modify": []}, results.staged)
  2503. self.assertListEqual(results.unstaged, [b"crlf"])
  2504. self.assertListEqual(results.untracked, [])
  2505. def test_status_autocrlf_true(self) -> None:
  2506. # First make a commit as if the file has been added on a Linux system
  2507. # or with core.autocrlf=True
  2508. file_path = os.path.join(self.repo.path, "crlf")
  2509. with open(file_path, "wb") as f:
  2510. f.write(b"line1\nline2")
  2511. porcelain.add(repo=self.repo.path, paths=[file_path])
  2512. porcelain.commit(
  2513. repo=self.repo.path,
  2514. message=b"test status",
  2515. author=b"author <email>",
  2516. committer=b"committer <email>",
  2517. )
  2518. # Then update the file as if it was created by CGit on a Windows
  2519. # system with core.autocrlf=true
  2520. with open(file_path, "wb") as f:
  2521. f.write(b"line1\r\nline2")
  2522. # TODO: It should be set automatically by looking at the configuration
  2523. c = self.repo.get_config()
  2524. c.set("core", "autocrlf", True)
  2525. c.write_to_path()
  2526. results = porcelain.status(self.repo)
  2527. self.assertDictEqual({"add": [], "delete": [], "modify": []}, results.staged)
  2528. self.assertListEqual(results.unstaged, [])
  2529. self.assertListEqual(results.untracked, [])
  2530. def test_status_autocrlf_input(self) -> None:
  2531. # Commit existing file with CRLF
  2532. file_path = os.path.join(self.repo.path, "crlf-exists")
  2533. with open(file_path, "wb") as f:
  2534. f.write(b"line1\r\nline2")
  2535. porcelain.add(repo=self.repo.path, paths=[file_path])
  2536. porcelain.commit(
  2537. repo=self.repo.path,
  2538. message=b"test status",
  2539. author=b"author <email>",
  2540. committer=b"committer <email>",
  2541. )
  2542. c = self.repo.get_config()
  2543. c.set("core", "autocrlf", "input")
  2544. c.write_to_path()
  2545. # Add new (untracked) file
  2546. file_path = os.path.join(self.repo.path, "crlf-new")
  2547. with open(file_path, "wb") as f:
  2548. f.write(b"line1\r\nline2")
  2549. porcelain.add(repo=self.repo.path, paths=[file_path])
  2550. results = porcelain.status(self.repo)
  2551. self.assertDictEqual(
  2552. {"add": [b"crlf-new"], "delete": [], "modify": []}, results.staged
  2553. )
  2554. self.assertListEqual(results.unstaged, [])
  2555. self.assertListEqual(results.untracked, [])
  2556. def test_get_tree_changes_add(self) -> None:
  2557. """Unit test for get_tree_changes add."""
  2558. # Make a dummy file, stage
  2559. filename = "bar"
  2560. fullpath = os.path.join(self.repo.path, filename)
  2561. with open(fullpath, "w") as f:
  2562. f.write("stuff")
  2563. porcelain.add(repo=self.repo.path, paths=fullpath)
  2564. porcelain.commit(
  2565. repo=self.repo.path,
  2566. message=b"test status",
  2567. author=b"author <email>",
  2568. committer=b"committer <email>",
  2569. )
  2570. filename = "foo"
  2571. fullpath = os.path.join(self.repo.path, filename)
  2572. with open(fullpath, "w") as f:
  2573. f.write("stuff")
  2574. porcelain.add(repo=self.repo.path, paths=fullpath)
  2575. changes = porcelain.get_tree_changes(self.repo.path)
  2576. self.assertEqual(changes["add"][0], filename.encode("ascii"))
  2577. self.assertEqual(len(changes["add"]), 1)
  2578. self.assertEqual(len(changes["modify"]), 0)
  2579. self.assertEqual(len(changes["delete"]), 0)
  2580. def test_get_tree_changes_modify(self) -> None:
  2581. """Unit test for get_tree_changes modify."""
  2582. # Make a dummy file, stage, commit, modify
  2583. filename = "foo"
  2584. fullpath = os.path.join(self.repo.path, filename)
  2585. with open(fullpath, "w") as f:
  2586. f.write("stuff")
  2587. porcelain.add(repo=self.repo.path, paths=fullpath)
  2588. porcelain.commit(
  2589. repo=self.repo.path,
  2590. message=b"test status",
  2591. author=b"author <email>",
  2592. committer=b"committer <email>",
  2593. )
  2594. with open(fullpath, "w") as f:
  2595. f.write("otherstuff")
  2596. porcelain.add(repo=self.repo.path, paths=fullpath)
  2597. changes = porcelain.get_tree_changes(self.repo.path)
  2598. self.assertEqual(changes["modify"][0], filename.encode("ascii"))
  2599. self.assertEqual(len(changes["add"]), 0)
  2600. self.assertEqual(len(changes["modify"]), 1)
  2601. self.assertEqual(len(changes["delete"]), 0)
  2602. def test_get_tree_changes_delete(self) -> None:
  2603. """Unit test for get_tree_changes delete."""
  2604. # Make a dummy file, stage, commit, remove
  2605. filename = "foo"
  2606. fullpath = os.path.join(self.repo.path, filename)
  2607. with open(fullpath, "w") as f:
  2608. f.write("stuff")
  2609. porcelain.add(repo=self.repo.path, paths=fullpath)
  2610. porcelain.commit(
  2611. repo=self.repo.path,
  2612. message=b"test status",
  2613. author=b"author <email>",
  2614. committer=b"committer <email>",
  2615. )
  2616. cwd = os.getcwd()
  2617. try:
  2618. os.chdir(self.repo.path)
  2619. porcelain.remove(repo=self.repo.path, paths=[filename])
  2620. finally:
  2621. os.chdir(cwd)
  2622. changes = porcelain.get_tree_changes(self.repo.path)
  2623. self.assertEqual(changes["delete"][0], filename.encode("ascii"))
  2624. self.assertEqual(len(changes["add"]), 0)
  2625. self.assertEqual(len(changes["modify"]), 0)
  2626. self.assertEqual(len(changes["delete"]), 1)
  2627. def test_get_untracked_paths(self) -> None:
  2628. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  2629. f.write("ignored\n")
  2630. with open(os.path.join(self.repo.path, "ignored"), "w") as f:
  2631. f.write("blah\n")
  2632. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  2633. f.write("blah\n")
  2634. os.symlink(
  2635. os.path.join(self.repo.path, os.pardir, "external_target"),
  2636. os.path.join(self.repo.path, "link"),
  2637. )
  2638. self.assertEqual(
  2639. {"ignored", "notignored", ".gitignore", "link"},
  2640. set(
  2641. porcelain.get_untracked_paths(
  2642. self.repo.path, self.repo.path, self.repo.open_index()
  2643. )
  2644. ),
  2645. )
  2646. self.assertEqual(
  2647. {".gitignore", "notignored", "link"},
  2648. set(porcelain.status(self.repo).untracked),
  2649. )
  2650. self.assertEqual(
  2651. {".gitignore", "notignored", "ignored", "link"},
  2652. set(porcelain.status(self.repo, ignored=True).untracked),
  2653. )
  2654. def test_get_untracked_paths_subrepo(self) -> None:
  2655. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  2656. f.write("nested/\n")
  2657. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  2658. f.write("blah\n")
  2659. subrepo = Repo.init(os.path.join(self.repo.path, "nested"), mkdir=True)
  2660. with open(os.path.join(subrepo.path, "ignored"), "w") as f:
  2661. f.write("bleep\n")
  2662. with open(os.path.join(subrepo.path, "with"), "w") as f:
  2663. f.write("bloop\n")
  2664. with open(os.path.join(subrepo.path, "manager"), "w") as f:
  2665. f.write("blop\n")
  2666. self.assertEqual(
  2667. {".gitignore", "notignored", os.path.join("nested", "")},
  2668. set(
  2669. porcelain.get_untracked_paths(
  2670. self.repo.path, self.repo.path, self.repo.open_index()
  2671. )
  2672. ),
  2673. )
  2674. self.assertEqual(
  2675. {".gitignore", "notignored"},
  2676. set(
  2677. porcelain.get_untracked_paths(
  2678. self.repo.path,
  2679. self.repo.path,
  2680. self.repo.open_index(),
  2681. exclude_ignored=True,
  2682. )
  2683. ),
  2684. )
  2685. self.assertEqual(
  2686. {"ignored", "with", "manager"},
  2687. set(
  2688. porcelain.get_untracked_paths(
  2689. subrepo.path, subrepo.path, subrepo.open_index()
  2690. )
  2691. ),
  2692. )
  2693. self.assertEqual(
  2694. set(),
  2695. set(
  2696. porcelain.get_untracked_paths(
  2697. subrepo.path,
  2698. self.repo.path,
  2699. self.repo.open_index(),
  2700. )
  2701. ),
  2702. )
  2703. self.assertEqual(
  2704. {
  2705. os.path.join("nested", "ignored"),
  2706. os.path.join("nested", "with"),
  2707. os.path.join("nested", "manager"),
  2708. },
  2709. set(
  2710. porcelain.get_untracked_paths(
  2711. self.repo.path,
  2712. subrepo.path,
  2713. self.repo.open_index(),
  2714. )
  2715. ),
  2716. )
  2717. def test_get_untracked_paths_subdir(self) -> None:
  2718. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  2719. f.write("subdir/\nignored")
  2720. with open(os.path.join(self.repo.path, "notignored"), "w") as f:
  2721. f.write("blah\n")
  2722. os.mkdir(os.path.join(self.repo.path, "subdir"))
  2723. with open(os.path.join(self.repo.path, "ignored"), "w") as f:
  2724. f.write("foo")
  2725. with open(os.path.join(self.repo.path, "subdir", "ignored"), "w") as f:
  2726. f.write("foo")
  2727. self.assertEqual(
  2728. {
  2729. ".gitignore",
  2730. "notignored",
  2731. "ignored",
  2732. os.path.join("subdir", ""),
  2733. },
  2734. set(
  2735. porcelain.get_untracked_paths(
  2736. self.repo.path,
  2737. self.repo.path,
  2738. self.repo.open_index(),
  2739. )
  2740. ),
  2741. )
  2742. self.assertEqual(
  2743. {".gitignore", "notignored"},
  2744. set(
  2745. porcelain.get_untracked_paths(
  2746. self.repo.path,
  2747. self.repo.path,
  2748. self.repo.open_index(),
  2749. exclude_ignored=True,
  2750. )
  2751. ),
  2752. )
  2753. def test_get_untracked_paths_invalid_untracked_files(self) -> None:
  2754. with self.assertRaises(ValueError):
  2755. list(
  2756. porcelain.get_untracked_paths(
  2757. self.repo.path,
  2758. self.repo.path,
  2759. self.repo.open_index(),
  2760. untracked_files="invalid_value",
  2761. )
  2762. )
  2763. def test_get_untracked_paths_normal(self) -> None:
  2764. with self.assertRaises(NotImplementedError):
  2765. _, _, _ = porcelain.status(repo=self.repo.path, untracked_files="normal")
  2766. # TODO(jelmer): Add test for dulwich.porcelain.daemon
  2767. class UploadPackTests(PorcelainTestCase):
  2768. """Tests for upload_pack."""
  2769. def test_upload_pack(self) -> None:
  2770. outf = BytesIO()
  2771. exitcode = porcelain.upload_pack(self.repo.path, BytesIO(b"0000"), outf)
  2772. outlines = outf.getvalue().splitlines()
  2773. self.assertEqual([b"0000"], outlines)
  2774. self.assertEqual(0, exitcode)
  2775. class ReceivePackTests(PorcelainTestCase):
  2776. """Tests for receive_pack."""
  2777. def test_receive_pack(self) -> None:
  2778. filename = "foo"
  2779. fullpath = os.path.join(self.repo.path, filename)
  2780. with open(fullpath, "w") as f:
  2781. f.write("stuff")
  2782. porcelain.add(repo=self.repo.path, paths=fullpath)
  2783. self.repo.do_commit(
  2784. message=b"test status",
  2785. author=b"author <email>",
  2786. committer=b"committer <email>",
  2787. author_timestamp=1402354300,
  2788. commit_timestamp=1402354300,
  2789. author_timezone=0,
  2790. commit_timezone=0,
  2791. )
  2792. outf = BytesIO()
  2793. exitcode = porcelain.receive_pack(self.repo.path, BytesIO(b"0000"), outf)
  2794. outlines = outf.getvalue().splitlines()
  2795. self.assertEqual(
  2796. [
  2797. b"0091319b56ce3aee2d489f759736a79cc552c9bb86d9 HEAD\x00 report-status "
  2798. b"delete-refs quiet ofs-delta side-band-64k "
  2799. b"no-done symref=HEAD:refs/heads/master",
  2800. b"003f319b56ce3aee2d489f759736a79cc552c9bb86d9 refs/heads/master",
  2801. b"0000",
  2802. ],
  2803. outlines,
  2804. )
  2805. self.assertEqual(0, exitcode)
  2806. class BranchListTests(PorcelainTestCase):
  2807. def test_standard(self) -> None:
  2808. self.assertEqual(set(), set(porcelain.branch_list(self.repo)))
  2809. def test_new_branch(self) -> None:
  2810. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  2811. self.repo[b"HEAD"] = c1.id
  2812. porcelain.branch_create(self.repo, b"foo")
  2813. self.assertEqual({b"master", b"foo"}, set(porcelain.branch_list(self.repo)))
  2814. class BranchCreateTests(PorcelainTestCase):
  2815. def test_branch_exists(self) -> None:
  2816. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  2817. self.repo[b"HEAD"] = c1.id
  2818. porcelain.branch_create(self.repo, b"foo")
  2819. self.assertRaises(porcelain.Error, porcelain.branch_create, self.repo, b"foo")
  2820. porcelain.branch_create(self.repo, b"foo", force=True)
  2821. def test_new_branch(self) -> None:
  2822. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  2823. self.repo[b"HEAD"] = c1.id
  2824. porcelain.branch_create(self.repo, b"foo")
  2825. self.assertEqual({b"master", b"foo"}, set(porcelain.branch_list(self.repo)))
  2826. class BranchDeleteTests(PorcelainTestCase):
  2827. def test_simple(self) -> None:
  2828. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  2829. self.repo[b"HEAD"] = c1.id
  2830. porcelain.branch_create(self.repo, b"foo")
  2831. self.assertIn(b"foo", porcelain.branch_list(self.repo))
  2832. porcelain.branch_delete(self.repo, b"foo")
  2833. self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
  2834. def test_simple_unicode(self) -> None:
  2835. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  2836. self.repo[b"HEAD"] = c1.id
  2837. porcelain.branch_create(self.repo, "foo")
  2838. self.assertIn(b"foo", porcelain.branch_list(self.repo))
  2839. porcelain.branch_delete(self.repo, "foo")
  2840. self.assertNotIn(b"foo", porcelain.branch_list(self.repo))
  2841. class FetchTests(PorcelainTestCase):
  2842. def test_simple(self) -> None:
  2843. outstream = BytesIO()
  2844. errstream = BytesIO()
  2845. # create a file for initial commit
  2846. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2847. os.close(handle)
  2848. porcelain.add(repo=self.repo.path, paths=fullpath)
  2849. porcelain.commit(
  2850. repo=self.repo.path,
  2851. message=b"test",
  2852. author=b"test <email>",
  2853. committer=b"test <email>",
  2854. )
  2855. # Setup target repo
  2856. target_path = tempfile.mkdtemp()
  2857. self.addCleanup(shutil.rmtree, target_path)
  2858. target_repo = porcelain.clone(
  2859. self.repo.path, target=target_path, errstream=errstream
  2860. )
  2861. # create a second file to be pushed
  2862. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2863. os.close(handle)
  2864. porcelain.add(repo=self.repo.path, paths=fullpath)
  2865. porcelain.commit(
  2866. repo=self.repo.path,
  2867. message=b"test2",
  2868. author=b"test2 <email>",
  2869. committer=b"test2 <email>",
  2870. )
  2871. self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
  2872. target_repo.close()
  2873. # Fetch changes into the cloned repo
  2874. porcelain.fetch(target_path, "origin", outstream=outstream, errstream=errstream)
  2875. # Assert that fetch updated the local image of the remote
  2876. self.assert_correct_remote_refs(target_repo.get_refs(), self.repo.get_refs())
  2877. # Check the target repo for pushed changes
  2878. with Repo(target_path) as r:
  2879. self.assertIn(self.repo[b"HEAD"].id, r)
  2880. def test_with_remote_name(self) -> None:
  2881. remote_name = "origin"
  2882. outstream = BytesIO()
  2883. errstream = BytesIO()
  2884. # create a file for initial commit
  2885. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2886. os.close(handle)
  2887. porcelain.add(repo=self.repo.path, paths=fullpath)
  2888. porcelain.commit(
  2889. repo=self.repo.path,
  2890. message=b"test",
  2891. author=b"test <email>",
  2892. committer=b"test <email>",
  2893. )
  2894. # Setup target repo
  2895. target_path = tempfile.mkdtemp()
  2896. self.addCleanup(shutil.rmtree, target_path)
  2897. target_repo = porcelain.clone(
  2898. self.repo.path, target=target_path, errstream=errstream
  2899. )
  2900. # Capture current refs
  2901. target_refs = target_repo.get_refs()
  2902. # create a second file to be pushed
  2903. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2904. os.close(handle)
  2905. porcelain.add(repo=self.repo.path, paths=fullpath)
  2906. porcelain.commit(
  2907. repo=self.repo.path,
  2908. message=b"test2",
  2909. author=b"test2 <email>",
  2910. committer=b"test2 <email>",
  2911. )
  2912. self.assertNotIn(self.repo[b"HEAD"].id, target_repo)
  2913. target_config = target_repo.get_config()
  2914. target_config.set(
  2915. (b"remote", remote_name.encode()), b"url", self.repo.path.encode()
  2916. )
  2917. target_repo.close()
  2918. # Fetch changes into the cloned repo
  2919. porcelain.fetch(
  2920. target_path, remote_name, outstream=outstream, errstream=errstream
  2921. )
  2922. # Assert that fetch updated the local image of the remote
  2923. self.assert_correct_remote_refs(target_repo.get_refs(), self.repo.get_refs())
  2924. # Check the target repo for pushed changes, as well as updates
  2925. # for the refs
  2926. with Repo(target_path) as r:
  2927. self.assertIn(self.repo[b"HEAD"].id, r)
  2928. self.assertNotEqual(self.repo.get_refs(), target_refs)
  2929. def assert_correct_remote_refs(
  2930. self, local_refs, remote_refs, remote_name=b"origin"
  2931. ) -> None:
  2932. """Assert that known remote refs corresponds to actual remote refs."""
  2933. local_ref_prefix = b"refs/heads"
  2934. remote_ref_prefix = b"refs/remotes/" + remote_name
  2935. locally_known_remote_refs = {
  2936. k[len(remote_ref_prefix) + 1 :]: v
  2937. for k, v in local_refs.items()
  2938. if k.startswith(remote_ref_prefix)
  2939. }
  2940. normalized_remote_refs = {
  2941. k[len(local_ref_prefix) + 1 :]: v
  2942. for k, v in remote_refs.items()
  2943. if k.startswith(local_ref_prefix)
  2944. }
  2945. if b"HEAD" in locally_known_remote_refs and b"HEAD" in remote_refs:
  2946. normalized_remote_refs[b"HEAD"] = remote_refs[b"HEAD"]
  2947. self.assertEqual(locally_known_remote_refs, normalized_remote_refs)
  2948. class RepackTests(PorcelainTestCase):
  2949. def test_empty(self) -> None:
  2950. porcelain.repack(self.repo)
  2951. def test_simple(self) -> None:
  2952. handle, fullpath = tempfile.mkstemp(dir=self.repo.path)
  2953. os.close(handle)
  2954. porcelain.add(repo=self.repo.path, paths=fullpath)
  2955. porcelain.repack(self.repo)
  2956. class LsTreeTests(PorcelainTestCase):
  2957. def test_empty(self) -> None:
  2958. porcelain.commit(
  2959. repo=self.repo.path,
  2960. message=b"test status",
  2961. author=b"author <email>",
  2962. committer=b"committer <email>",
  2963. )
  2964. f = StringIO()
  2965. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  2966. self.assertEqual(f.getvalue(), "")
  2967. def test_simple(self) -> None:
  2968. # Commit a dummy file then modify it
  2969. fullpath = os.path.join(self.repo.path, "foo")
  2970. with open(fullpath, "w") as f:
  2971. f.write("origstuff")
  2972. porcelain.add(repo=self.repo.path, paths=[fullpath])
  2973. porcelain.commit(
  2974. repo=self.repo.path,
  2975. message=b"test status",
  2976. author=b"author <email>",
  2977. committer=b"committer <email>",
  2978. )
  2979. f = StringIO()
  2980. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  2981. self.assertEqual(
  2982. f.getvalue(),
  2983. "100644 blob 8b82634d7eae019850bb883f06abf428c58bc9aa\tfoo\n",
  2984. )
  2985. def test_recursive(self) -> None:
  2986. # Create a directory then write a dummy file in it
  2987. dirpath = os.path.join(self.repo.path, "adir")
  2988. filepath = os.path.join(dirpath, "afile")
  2989. os.mkdir(dirpath)
  2990. with open(filepath, "w") as f:
  2991. f.write("origstuff")
  2992. porcelain.add(repo=self.repo.path, paths=[filepath])
  2993. porcelain.commit(
  2994. repo=self.repo.path,
  2995. message=b"test status",
  2996. author=b"author <email>",
  2997. committer=b"committer <email>",
  2998. )
  2999. f = StringIO()
  3000. porcelain.ls_tree(self.repo, b"HEAD", outstream=f)
  3001. self.assertEqual(
  3002. f.getvalue(),
  3003. "40000 tree b145cc69a5e17693e24d8a7be0016ed8075de66d\tadir\n",
  3004. )
  3005. f = StringIO()
  3006. porcelain.ls_tree(self.repo, b"HEAD", outstream=f, recursive=True)
  3007. self.assertEqual(
  3008. f.getvalue(),
  3009. "40000 tree b145cc69a5e17693e24d8a7be0016ed8075de66d\tadir\n"
  3010. "100644 blob 8b82634d7eae019850bb883f06abf428c58bc9aa\tadir"
  3011. "/afile\n",
  3012. )
  3013. class LsRemoteTests(PorcelainTestCase):
  3014. def test_empty(self) -> None:
  3015. self.assertEqual({}, porcelain.ls_remote(self.repo.path))
  3016. def test_some(self) -> None:
  3017. cid = porcelain.commit(
  3018. repo=self.repo.path,
  3019. message=b"test status",
  3020. author=b"author <email>",
  3021. committer=b"committer <email>",
  3022. )
  3023. self.assertEqual(
  3024. {b"refs/heads/master": cid, b"HEAD": cid},
  3025. porcelain.ls_remote(self.repo.path),
  3026. )
  3027. class LsFilesTests(PorcelainTestCase):
  3028. def test_empty(self) -> None:
  3029. self.assertEqual([], list(porcelain.ls_files(self.repo)))
  3030. def test_simple(self) -> None:
  3031. # Commit a dummy file then modify it
  3032. fullpath = os.path.join(self.repo.path, "foo")
  3033. with open(fullpath, "w") as f:
  3034. f.write("origstuff")
  3035. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3036. self.assertEqual([b"foo"], list(porcelain.ls_files(self.repo)))
  3037. class RemoteAddTests(PorcelainTestCase):
  3038. def test_new(self) -> None:
  3039. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3040. c = self.repo.get_config()
  3041. self.assertEqual(
  3042. c.get((b"remote", b"jelmer"), b"url"),
  3043. b"git://jelmer.uk/code/dulwich",
  3044. )
  3045. def test_exists(self) -> None:
  3046. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3047. self.assertRaises(
  3048. porcelain.RemoteExists,
  3049. porcelain.remote_add,
  3050. self.repo,
  3051. "jelmer",
  3052. "git://jelmer.uk/code/dulwich",
  3053. )
  3054. class RemoteRemoveTests(PorcelainTestCase):
  3055. def test_remove(self) -> None:
  3056. porcelain.remote_add(self.repo, "jelmer", "git://jelmer.uk/code/dulwich")
  3057. c = self.repo.get_config()
  3058. self.assertEqual(
  3059. c.get((b"remote", b"jelmer"), b"url"),
  3060. b"git://jelmer.uk/code/dulwich",
  3061. )
  3062. porcelain.remote_remove(self.repo, "jelmer")
  3063. self.assertRaises(KeyError, porcelain.remote_remove, self.repo, "jelmer")
  3064. c = self.repo.get_config()
  3065. self.assertRaises(KeyError, c.get, (b"remote", b"jelmer"), b"url")
  3066. class CheckIgnoreTests(PorcelainTestCase):
  3067. def test_check_ignored(self) -> None:
  3068. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3069. f.write("foo")
  3070. foo_path = os.path.join(self.repo.path, "foo")
  3071. with open(foo_path, "w") as f:
  3072. f.write("BAR")
  3073. bar_path = os.path.join(self.repo.path, "bar")
  3074. with open(bar_path, "w") as f:
  3075. f.write("BAR")
  3076. self.assertEqual(["foo"], list(porcelain.check_ignore(self.repo, [foo_path])))
  3077. self.assertEqual([], list(porcelain.check_ignore(self.repo, [bar_path])))
  3078. def test_check_added_abs(self) -> None:
  3079. path = os.path.join(self.repo.path, "foo")
  3080. with open(path, "w") as f:
  3081. f.write("BAR")
  3082. self.repo.stage(["foo"])
  3083. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3084. f.write("foo\n")
  3085. self.assertEqual([], list(porcelain.check_ignore(self.repo, [path])))
  3086. self.assertEqual(
  3087. ["foo"],
  3088. list(porcelain.check_ignore(self.repo, [path], no_index=True)),
  3089. )
  3090. def test_check_added_rel(self) -> None:
  3091. with open(os.path.join(self.repo.path, "foo"), "w") as f:
  3092. f.write("BAR")
  3093. self.repo.stage(["foo"])
  3094. with open(os.path.join(self.repo.path, ".gitignore"), "w") as f:
  3095. f.write("foo\n")
  3096. cwd = os.getcwd()
  3097. os.mkdir(os.path.join(self.repo.path, "bar"))
  3098. os.chdir(os.path.join(self.repo.path, "bar"))
  3099. try:
  3100. self.assertEqual(list(porcelain.check_ignore(self.repo, ["../foo"])), [])
  3101. self.assertEqual(
  3102. ["../foo"],
  3103. list(porcelain.check_ignore(self.repo, ["../foo"], no_index=True)),
  3104. )
  3105. finally:
  3106. os.chdir(cwd)
  3107. class UpdateHeadTests(PorcelainTestCase):
  3108. def test_set_to_branch(self) -> None:
  3109. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3110. self.repo.refs[b"refs/heads/blah"] = c1.id
  3111. porcelain.update_head(self.repo, "blah")
  3112. self.assertEqual(c1.id, self.repo.head())
  3113. self.assertEqual(b"ref: refs/heads/blah", self.repo.refs.read_ref(b"HEAD"))
  3114. def test_set_to_branch_detached(self) -> None:
  3115. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3116. self.repo.refs[b"refs/heads/blah"] = c1.id
  3117. porcelain.update_head(self.repo, "blah", detached=True)
  3118. self.assertEqual(c1.id, self.repo.head())
  3119. self.assertEqual(c1.id, self.repo.refs.read_ref(b"HEAD"))
  3120. def test_set_to_commit_detached(self) -> None:
  3121. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3122. self.repo.refs[b"refs/heads/blah"] = c1.id
  3123. porcelain.update_head(self.repo, c1.id, detached=True)
  3124. self.assertEqual(c1.id, self.repo.head())
  3125. self.assertEqual(c1.id, self.repo.refs.read_ref(b"HEAD"))
  3126. def test_set_new_branch(self) -> None:
  3127. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3128. self.repo.refs[b"refs/heads/blah"] = c1.id
  3129. porcelain.update_head(self.repo, "blah", new_branch="bar")
  3130. self.assertEqual(c1.id, self.repo.head())
  3131. self.assertEqual(b"ref: refs/heads/bar", self.repo.refs.read_ref(b"HEAD"))
  3132. class MailmapTests(PorcelainTestCase):
  3133. def test_no_mailmap(self) -> None:
  3134. self.assertEqual(
  3135. b"Jelmer Vernooij <jelmer@samba.org>",
  3136. porcelain.check_mailmap(self.repo, b"Jelmer Vernooij <jelmer@samba.org>"),
  3137. )
  3138. def test_mailmap_lookup(self) -> None:
  3139. with open(os.path.join(self.repo.path, ".mailmap"), "wb") as f:
  3140. f.write(
  3141. b"""\
  3142. Jelmer Vernooij <jelmer@debian.org>
  3143. """
  3144. )
  3145. self.assertEqual(
  3146. b"Jelmer Vernooij <jelmer@debian.org>",
  3147. porcelain.check_mailmap(self.repo, b"Jelmer Vernooij <jelmer@samba.org>"),
  3148. )
  3149. class FsckTests(PorcelainTestCase):
  3150. def test_none(self) -> None:
  3151. self.assertEqual([], list(porcelain.fsck(self.repo)))
  3152. def test_git_dir(self) -> None:
  3153. obj = Tree()
  3154. a = Blob()
  3155. a.data = b"foo"
  3156. obj.add(b".git", 0o100644, a.id)
  3157. self.repo.object_store.add_objects([(a, None), (obj, None)])
  3158. self.assertEqual(
  3159. [(obj.id, "invalid name .git")],
  3160. [(sha, str(e)) for (sha, e) in porcelain.fsck(self.repo)],
  3161. )
  3162. class DescribeTests(PorcelainTestCase):
  3163. def test_no_commits(self) -> None:
  3164. self.assertRaises(KeyError, porcelain.describe, self.repo.path)
  3165. def test_single_commit(self) -> None:
  3166. fullpath = os.path.join(self.repo.path, "foo")
  3167. with open(fullpath, "w") as f:
  3168. f.write("BAR")
  3169. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3170. sha = porcelain.commit(
  3171. self.repo.path,
  3172. message=b"Some message",
  3173. author=b"Joe <joe@example.com>",
  3174. committer=b"Bob <bob@example.com>",
  3175. )
  3176. self.assertEqual(
  3177. "g{}".format(sha[:7].decode("ascii")),
  3178. porcelain.describe(self.repo.path),
  3179. )
  3180. def test_tag(self) -> None:
  3181. fullpath = os.path.join(self.repo.path, "foo")
  3182. with open(fullpath, "w") as f:
  3183. f.write("BAR")
  3184. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3185. porcelain.commit(
  3186. self.repo.path,
  3187. message=b"Some message",
  3188. author=b"Joe <joe@example.com>",
  3189. committer=b"Bob <bob@example.com>",
  3190. )
  3191. porcelain.tag_create(
  3192. self.repo.path,
  3193. b"tryme",
  3194. b"foo <foo@bar.com>",
  3195. b"bar",
  3196. annotated=True,
  3197. )
  3198. self.assertEqual("tryme", porcelain.describe(self.repo.path))
  3199. def test_tag_and_commit(self) -> None:
  3200. fullpath = os.path.join(self.repo.path, "foo")
  3201. with open(fullpath, "w") as f:
  3202. f.write("BAR")
  3203. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3204. porcelain.commit(
  3205. self.repo.path,
  3206. message=b"Some message",
  3207. author=b"Joe <joe@example.com>",
  3208. committer=b"Bob <bob@example.com>",
  3209. )
  3210. porcelain.tag_create(
  3211. self.repo.path,
  3212. b"tryme",
  3213. b"foo <foo@bar.com>",
  3214. b"bar",
  3215. annotated=True,
  3216. )
  3217. with open(fullpath, "w") as f:
  3218. f.write("BAR2")
  3219. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3220. sha = porcelain.commit(
  3221. self.repo.path,
  3222. message=b"Some message",
  3223. author=b"Joe <joe@example.com>",
  3224. committer=b"Bob <bob@example.com>",
  3225. )
  3226. self.assertEqual(
  3227. "tryme-1-g{}".format(sha[:7].decode("ascii")),
  3228. porcelain.describe(self.repo.path),
  3229. )
  3230. def test_tag_and_commit_full(self) -> None:
  3231. fullpath = os.path.join(self.repo.path, "foo")
  3232. with open(fullpath, "w") as f:
  3233. f.write("BAR")
  3234. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3235. porcelain.commit(
  3236. self.repo.path,
  3237. message=b"Some message",
  3238. author=b"Joe <joe@example.com>",
  3239. committer=b"Bob <bob@example.com>",
  3240. )
  3241. porcelain.tag_create(
  3242. self.repo.path,
  3243. b"tryme",
  3244. b"foo <foo@bar.com>",
  3245. b"bar",
  3246. annotated=True,
  3247. )
  3248. with open(fullpath, "w") as f:
  3249. f.write("BAR2")
  3250. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3251. sha = porcelain.commit(
  3252. self.repo.path,
  3253. message=b"Some message",
  3254. author=b"Joe <joe@example.com>",
  3255. committer=b"Bob <bob@example.com>",
  3256. )
  3257. self.assertEqual(
  3258. "tryme-1-g{}".format(sha.decode("ascii")),
  3259. porcelain.describe(self.repo.path, abbrev=40),
  3260. )
  3261. def test_untagged_commit_abbreviation(self) -> None:
  3262. _, _, c3 = build_commit_graph(self.repo.object_store, [[1], [2, 1], [3, 1, 2]])
  3263. self.repo.refs[b"HEAD"] = c3.id
  3264. brief_description, complete_description = (
  3265. porcelain.describe(self.repo),
  3266. porcelain.describe(self.repo, abbrev=40),
  3267. )
  3268. self.assertTrue(complete_description.startswith(brief_description))
  3269. self.assertEqual(
  3270. "g{}".format(c3.id.decode("ascii")),
  3271. complete_description,
  3272. )
  3273. class PathToTreeTests(PorcelainTestCase):
  3274. def setUp(self) -> None:
  3275. super().setUp()
  3276. self.fp = os.path.join(self.test_dir, "bar")
  3277. with open(self.fp, "w") as f:
  3278. f.write("something")
  3279. oldcwd = os.getcwd()
  3280. self.addCleanup(os.chdir, oldcwd)
  3281. os.chdir(self.test_dir)
  3282. def test_path_to_tree_path_base(self) -> None:
  3283. self.assertEqual(b"bar", porcelain.path_to_tree_path(self.test_dir, self.fp))
  3284. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "./bar"))
  3285. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "bar"))
  3286. cwd = os.getcwd()
  3287. self.assertEqual(
  3288. b"bar", porcelain.path_to_tree_path(".", os.path.join(cwd, "bar"))
  3289. )
  3290. self.assertEqual(b"bar", porcelain.path_to_tree_path(cwd, "bar"))
  3291. def test_path_to_tree_path_syntax(self) -> None:
  3292. self.assertEqual(b"bar", porcelain.path_to_tree_path(".", "./bar"))
  3293. def test_path_to_tree_path_error(self) -> None:
  3294. with self.assertRaises(ValueError):
  3295. with tempfile.TemporaryDirectory() as od:
  3296. porcelain.path_to_tree_path(od, self.fp)
  3297. def test_path_to_tree_path_rel(self) -> None:
  3298. cwd = os.getcwd()
  3299. os.mkdir(os.path.join(self.repo.path, "foo"))
  3300. os.mkdir(os.path.join(self.repo.path, "foo/bar"))
  3301. try:
  3302. os.chdir(os.path.join(self.repo.path, "foo/bar"))
  3303. with open("baz", "w") as f:
  3304. f.write("contents")
  3305. self.assertEqual(b"bar/baz", porcelain.path_to_tree_path("..", "baz"))
  3306. self.assertEqual(
  3307. b"bar/baz",
  3308. porcelain.path_to_tree_path(
  3309. os.path.join(os.getcwd(), ".."),
  3310. os.path.join(os.getcwd(), "baz"),
  3311. ),
  3312. )
  3313. self.assertEqual(
  3314. b"bar/baz",
  3315. porcelain.path_to_tree_path("..", os.path.join(os.getcwd(), "baz")),
  3316. )
  3317. self.assertEqual(
  3318. b"bar/baz",
  3319. porcelain.path_to_tree_path(os.path.join(os.getcwd(), ".."), "baz"),
  3320. )
  3321. finally:
  3322. os.chdir(cwd)
  3323. class GetObjectByPathTests(PorcelainTestCase):
  3324. def test_simple(self) -> None:
  3325. fullpath = os.path.join(self.repo.path, "foo")
  3326. with open(fullpath, "w") as f:
  3327. f.write("BAR")
  3328. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3329. porcelain.commit(
  3330. self.repo.path,
  3331. message=b"Some message",
  3332. author=b"Joe <joe@example.com>",
  3333. committer=b"Bob <bob@example.com>",
  3334. )
  3335. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, "foo").data)
  3336. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, b"foo").data)
  3337. def test_encoding(self) -> None:
  3338. fullpath = os.path.join(self.repo.path, "foo")
  3339. with open(fullpath, "w") as f:
  3340. f.write("BAR")
  3341. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3342. porcelain.commit(
  3343. self.repo.path,
  3344. message=b"Some message",
  3345. author=b"Joe <joe@example.com>",
  3346. committer=b"Bob <bob@example.com>",
  3347. encoding=b"utf-8",
  3348. )
  3349. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, "foo").data)
  3350. self.assertEqual(b"BAR", porcelain.get_object_by_path(self.repo, b"foo").data)
  3351. def test_missing(self) -> None:
  3352. self.assertRaises(KeyError, porcelain.get_object_by_path, self.repo, "foo")
  3353. class WriteTreeTests(PorcelainTestCase):
  3354. def test_simple(self) -> None:
  3355. fullpath = os.path.join(self.repo.path, "foo")
  3356. with open(fullpath, "w") as f:
  3357. f.write("BAR")
  3358. porcelain.add(repo=self.repo.path, paths=[fullpath])
  3359. self.assertEqual(
  3360. b"d2092c8a9f311f0311083bf8d177f2ca0ab5b241",
  3361. porcelain.write_tree(self.repo),
  3362. )
  3363. class ActiveBranchTests(PorcelainTestCase):
  3364. def test_simple(self) -> None:
  3365. self.assertEqual(b"master", porcelain.active_branch(self.repo))
  3366. class FindUniqueAbbrevTests(PorcelainTestCase):
  3367. def test_simple(self) -> None:
  3368. c1, c2, c3 = build_commit_graph(
  3369. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  3370. )
  3371. self.repo.refs[b"HEAD"] = c3.id
  3372. self.assertEqual(
  3373. c1.id.decode("ascii")[:7],
  3374. porcelain.find_unique_abbrev(self.repo.object_store, c1.id),
  3375. )
  3376. class PackRefsTests(PorcelainTestCase):
  3377. def test_all(self) -> None:
  3378. c1, c2, c3 = build_commit_graph(
  3379. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  3380. )
  3381. self.repo.refs[b"HEAD"] = c3.id
  3382. self.repo.refs[b"refs/heads/master"] = c2.id
  3383. self.repo.refs[b"refs/tags/foo"] = c1.id
  3384. porcelain.pack_refs(self.repo, all=True)
  3385. self.assertEqual(
  3386. self.repo.refs.get_packed_refs(),
  3387. {
  3388. b"refs/heads/master": c2.id,
  3389. b"refs/tags/foo": c1.id,
  3390. },
  3391. )
  3392. def test_not_all(self) -> None:
  3393. c1, c2, c3 = build_commit_graph(
  3394. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  3395. )
  3396. self.repo.refs[b"HEAD"] = c3.id
  3397. self.repo.refs[b"refs/heads/master"] = c2.id
  3398. self.repo.refs[b"refs/tags/foo"] = c1.id
  3399. porcelain.pack_refs(self.repo)
  3400. self.assertEqual(
  3401. self.repo.refs.get_packed_refs(),
  3402. {
  3403. b"refs/tags/foo": c1.id,
  3404. },
  3405. )
  3406. class ServerTests(PorcelainTestCase):
  3407. @contextlib.contextmanager
  3408. def _serving(self):
  3409. with make_server("localhost", 0, self.app) as server:
  3410. thread = threading.Thread(target=server.serve_forever, daemon=True)
  3411. thread.start()
  3412. try:
  3413. yield f"http://localhost:{server.server_port}"
  3414. finally:
  3415. server.shutdown()
  3416. thread.join(10)
  3417. def setUp(self) -> None:
  3418. super().setUp()
  3419. self.served_repo_path = os.path.join(self.test_dir, "served_repo.git")
  3420. self.served_repo = Repo.init_bare(self.served_repo_path, mkdir=True)
  3421. self.addCleanup(self.served_repo.close)
  3422. backend = DictBackend({"/": self.served_repo})
  3423. self.app = make_wsgi_chain(backend)
  3424. def test_pull(self) -> None:
  3425. (c1,) = build_commit_graph(self.served_repo.object_store, [[1]])
  3426. self.served_repo.refs[b"refs/heads/master"] = c1.id
  3427. with self._serving() as url:
  3428. porcelain.pull(self.repo, url, "master")
  3429. def test_push(self) -> None:
  3430. (c1,) = build_commit_graph(self.repo.object_store, [[1]])
  3431. self.repo.refs[b"refs/heads/master"] = c1.id
  3432. with self._serving() as url:
  3433. porcelain.push(self.repo, url, "master")
  3434. class ForEachTests(PorcelainTestCase):
  3435. def setUp(self) -> None:
  3436. super().setUp()
  3437. c1, c2, c3, c4 = build_commit_graph(
  3438. self.repo.object_store, [[1], [2, 1], [3, 1, 2], [4]]
  3439. )
  3440. porcelain.tag_create(
  3441. self.repo.path,
  3442. b"v0.1",
  3443. objectish=c1.id,
  3444. annotated=True,
  3445. message=b"0.1",
  3446. )
  3447. porcelain.tag_create(
  3448. self.repo.path,
  3449. b"v1.0",
  3450. objectish=c2.id,
  3451. annotated=True,
  3452. message=b"1.0",
  3453. )
  3454. porcelain.tag_create(self.repo.path, b"simple-tag", objectish=c3.id)
  3455. porcelain.tag_create(
  3456. self.repo.path,
  3457. b"v1.1",
  3458. objectish=c4.id,
  3459. annotated=True,
  3460. message=b"1.1",
  3461. )
  3462. porcelain.branch_create(
  3463. self.repo.path, b"feat", objectish=c2.id.decode("ascii")
  3464. )
  3465. self.repo.refs[b"HEAD"] = c4.id
  3466. def test_for_each_ref(self) -> None:
  3467. refs = porcelain.for_each_ref(self.repo)
  3468. self.assertEqual(
  3469. [(object_type, tag) for _, object_type, tag in refs],
  3470. [
  3471. (b"commit", b"refs/heads/feat"),
  3472. (b"commit", b"refs/heads/master"),
  3473. (b"commit", b"refs/tags/simple-tag"),
  3474. (b"tag", b"refs/tags/v0.1"),
  3475. (b"tag", b"refs/tags/v1.0"),
  3476. (b"tag", b"refs/tags/v1.1"),
  3477. ],
  3478. )
  3479. def test_for_each_ref_pattern(self) -> None:
  3480. versions = porcelain.for_each_ref(self.repo, pattern="refs/tags/v*")
  3481. self.assertEqual(
  3482. [(object_type, tag) for _, object_type, tag in versions],
  3483. [
  3484. (b"tag", b"refs/tags/v0.1"),
  3485. (b"tag", b"refs/tags/v1.0"),
  3486. (b"tag", b"refs/tags/v1.1"),
  3487. ],
  3488. )
  3489. versions = porcelain.for_each_ref(self.repo, pattern="refs/tags/v1.?")
  3490. self.assertEqual(
  3491. [(object_type, tag) for _, object_type, tag in versions],
  3492. [
  3493. (b"tag", b"refs/tags/v1.0"),
  3494. (b"tag", b"refs/tags/v1.1"),
  3495. ],
  3496. )
  3497. class SparseCheckoutTests(PorcelainTestCase):
  3498. """Integration tests for Dulwich's sparse checkout feature."""
  3499. # NOTE: We do NOT override `setUp()` here because the parent class
  3500. # (PorcelainTestCase) already:
  3501. # 1) Creates self.test_dir = a unique temp dir
  3502. # 2) Creates a subdir named "repo"
  3503. # 3) Calls Repo.init() on that path
  3504. # Re-initializing again caused FileExistsError.
  3505. #
  3506. # Utility/Placeholder
  3507. #
  3508. def sparse_checkout(self, repo, patterns, force=False):
  3509. """Wrapper around the actual porcelain.sparse_checkout function
  3510. to handle any test-specific setup or logging.
  3511. """
  3512. return porcelain.sparse_checkout(repo, patterns, force=force)
  3513. def _write_file(self, rel_path, content):
  3514. """Helper to write a file in the repository working tree."""
  3515. abs_path = os.path.join(self.repo_path, rel_path)
  3516. os.makedirs(os.path.dirname(abs_path), exist_ok=True)
  3517. with open(abs_path, "w") as f:
  3518. f.write(content)
  3519. return abs_path
  3520. def _commit_file(self, rel_path, content):
  3521. """Helper to write, add, and commit a file."""
  3522. abs_path = self._write_file(rel_path, content)
  3523. add(self.repo_path, paths=[abs_path])
  3524. commit(self.repo_path, message=b"Added " + rel_path.encode("utf-8"))
  3525. def _list_wtree_files(self):
  3526. """Return a set of all files (not dirs) present
  3527. in the working tree, ignoring .git/.
  3528. """
  3529. found_files = set()
  3530. for root, dirs, files in os.walk(self.repo_path):
  3531. # Skip .git in the walk
  3532. if ".git" in dirs:
  3533. dirs.remove(".git")
  3534. for filename in files:
  3535. file_rel = os.path.relpath(os.path.join(root, filename), self.repo_path)
  3536. found_files.add(file_rel)
  3537. return found_files
  3538. def test_only_included_paths_appear_in_wtree(self):
  3539. """Only included paths remain in the working tree, excluded paths are removed.
  3540. Commits two files, "keep_me.txt" and "exclude_me.txt". Then applies a
  3541. sparse-checkout pattern containing only "keep_me.txt". Ensures that
  3542. the latter remains in the working tree, while "exclude_me.txt" is
  3543. removed. This verifies correct application of sparse-checkout patterns
  3544. to remove files not listed.
  3545. """
  3546. self._commit_file("keep_me.txt", "I'll stay\n")
  3547. self._commit_file("exclude_me.txt", "I'll be excluded\n")
  3548. patterns = ["keep_me.txt"]
  3549. self.sparse_checkout(self.repo, patterns)
  3550. actual_files = self._list_wtree_files()
  3551. expected_files = {"keep_me.txt"}
  3552. self.assertEqual(
  3553. expected_files,
  3554. actual_files,
  3555. f"Expected only {expected_files}, but found {actual_files}",
  3556. )
  3557. def test_previously_included_paths_become_excluded(self):
  3558. """Previously included files become excluded after pattern changes.
  3559. Verifies that files initially brought into the working tree (e.g.,
  3560. by including `data/`) can later be excluded by narrowing the
  3561. sparse-checkout pattern to just `data/included_1.txt`. Confirms that
  3562. the file `data/included_2.txt` remains in the index with
  3563. skip-worktree set (rather than being removed entirely), ensuring
  3564. data is not lost and Dulwich correctly updates the index flags.
  3565. """
  3566. self._commit_file("data/included_1.txt", "some content\n")
  3567. self._commit_file("data/included_2.txt", "other content\n")
  3568. initial_patterns = ["data/"]
  3569. self.sparse_checkout(self.repo, initial_patterns)
  3570. updated_patterns = ["data/included_1.txt"]
  3571. self.sparse_checkout(self.repo, updated_patterns)
  3572. actual_files = self._list_wtree_files()
  3573. expected_files = {os.path.join("data", "included_1.txt")}
  3574. self.assertEqual(expected_files, actual_files)
  3575. idx = self.repo.open_index()
  3576. self.assertIn(b"data/included_2.txt", idx)
  3577. entry = idx[b"data/included_2.txt"]
  3578. self.assertTrue(entry.skip_worktree)
  3579. def test_force_removes_local_changes_for_excluded_paths(self):
  3580. """Forced sparse checkout removes local modifications for newly excluded paths.
  3581. Verifies that specifying force=True allows destructive operations
  3582. which discard uncommitted changes. First, we commit "file1.txt" and
  3583. then modify it. Next, we apply a pattern that excludes the file,
  3584. using force=True. The local modifications (and the file) should
  3585. be removed, leaving the working tree empty.
  3586. """
  3587. self._commit_file("file1.txt", "original content\n")
  3588. file1_path = os.path.join(self.repo_path, "file1.txt")
  3589. with open(file1_path, "a") as f:
  3590. f.write("local changes!\n")
  3591. new_patterns = ["some_other_file.txt"]
  3592. self.sparse_checkout(self.repo, new_patterns, force=True)
  3593. actual_files = self._list_wtree_files()
  3594. self.assertEqual(
  3595. set(),
  3596. actual_files,
  3597. "Force-sparse-checkout did not remove file with local changes.",
  3598. )
  3599. def test_destructive_refuse_uncommitted_changes_without_force(self):
  3600. """Fail on uncommitted changes for newly excluded paths without force.
  3601. Ensures that a sparse checkout is blocked if it would remove local
  3602. modifications from the working tree. We commit 'config.yaml', then
  3603. modify it, and finally attempt to exclude it via new patterns without
  3604. using force=True. This should raise a CheckoutError rather than
  3605. discarding the local changes.
  3606. """
  3607. self._commit_file("config.yaml", "initial\n")
  3608. cfg_path = os.path.join(self.repo_path, "config.yaml")
  3609. with open(cfg_path, "a") as f:
  3610. f.write("local modifications\n")
  3611. exclude_patterns = ["docs/"]
  3612. with self.assertRaises(CheckoutError):
  3613. self.sparse_checkout(self.repo, exclude_patterns, force=False)
  3614. def test_fnmatch_gitignore_pattern_expansion(self):
  3615. """Reading/writing patterns align with gitignore/fnmatch expansions.
  3616. Ensures that `sparse_checkout` interprets wildcard patterns (like `*.py`)
  3617. in the same way Git's sparse-checkout would. Multiple files are committed
  3618. to `src/` (e.g. `foo.py`, `foo_test.py`, `foo_helper.py`) and to `docs/`.
  3619. Then the pattern `src/foo*.py` is applied, confirming that only the
  3620. matching Python files remain in the working tree while the Markdown file
  3621. under `docs/` is excluded.
  3622. Finally, verifies that the `.git/info/sparse-checkout` file contains the
  3623. specified wildcard pattern (`src/foo*.py`), ensuring correct round-trip
  3624. of user-supplied patterns.
  3625. """
  3626. self._commit_file("src/foo.py", "print('hello')\n")
  3627. self._commit_file("src/foo_test.py", "print('test')\n")
  3628. self._commit_file("docs/readme.md", "# docs\n")
  3629. self._commit_file("src/foo_helper.py", "print('helper')\n")
  3630. patterns = ["src/foo*.py"]
  3631. self.sparse_checkout(self.repo, patterns)
  3632. actual_files = self._list_wtree_files()
  3633. expected_files = {
  3634. os.path.join("src", "foo.py"),
  3635. os.path.join("src", "foo_test.py"),
  3636. os.path.join("src", "foo_helper.py"),
  3637. }
  3638. self.assertEqual(
  3639. expected_files,
  3640. actual_files,
  3641. "Wildcard pattern not matched as expected. Either too strict or too broad.",
  3642. )
  3643. sc_file = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  3644. self.assertTrue(os.path.isfile(sc_file))
  3645. with open(sc_file) as f:
  3646. lines = f.read().strip().split()
  3647. self.assertIn("src/foo*.py", lines)
  3648. class ConeModeTests(PorcelainTestCase):
  3649. """Provide integration tests for Dulwich's cone mode sparse checkout.
  3650. This test suite verifies the expected behavior for:
  3651. * cone_mode_init
  3652. * cone_mode_set
  3653. * cone_mode_add
  3654. Although Dulwich does not yet implement cone mode, these tests are
  3655. prepared in advance to guide future development.
  3656. """
  3657. def setUp(self):
  3658. """Set up a fresh repository for each test.
  3659. This method creates a new empty repo_path and Repo object
  3660. as provided by the PorcelainTestCase base class.
  3661. """
  3662. super().setUp()
  3663. def _commit_file(self, rel_path, content=b"contents"):
  3664. """Add a file at the given relative path and commit it.
  3665. Creates necessary directories, writes the file content,
  3666. stages, and commits. The commit message and author/committer
  3667. are also provided.
  3668. """
  3669. full_path = os.path.join(self.repo_path, rel_path)
  3670. os.makedirs(os.path.dirname(full_path), exist_ok=True)
  3671. with open(full_path, "wb") as f:
  3672. f.write(content)
  3673. porcelain.add(self.repo_path, paths=[full_path])
  3674. porcelain.commit(
  3675. self.repo_path,
  3676. message=b"Adding " + rel_path.encode("utf-8"),
  3677. author=b"Test Author <author@example.com>",
  3678. committer=b"Test Committer <committer@example.com>",
  3679. )
  3680. def _list_wtree_files(self):
  3681. """Return a set of all file paths relative to the repository root.
  3682. Walks the working tree, skipping the .git directory.
  3683. """
  3684. found_files = set()
  3685. for root, dirs, files in os.walk(self.repo_path):
  3686. if ".git" in dirs:
  3687. dirs.remove(".git")
  3688. for fn in files:
  3689. relp = os.path.relpath(os.path.join(root, fn), self.repo_path)
  3690. found_files.add(relp)
  3691. return found_files
  3692. def test_init_excludes_everything(self):
  3693. """Verify that cone_mode_init writes minimal patterns and empties the working tree.
  3694. Make some dummy files, commit them, then call cone_mode_init. Confirm
  3695. that the working tree is empty, the sparse-checkout file has the
  3696. minimal patterns (/*, !/*/), and the relevant config values are set.
  3697. """
  3698. self._commit_file("docs/readme.md", b"# doc\n")
  3699. self._commit_file("src/main.py", b"print('hello')\n")
  3700. porcelain.cone_mode_init(self.repo)
  3701. actual_files = self._list_wtree_files()
  3702. self.assertEqual(
  3703. set(),
  3704. actual_files,
  3705. "cone_mode_init did not exclude all files from the working tree.",
  3706. )
  3707. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  3708. with open(sp_path) as f:
  3709. lines = [ln.strip() for ln in f if ln.strip()]
  3710. self.assertIn("/*", lines)
  3711. self.assertIn("!/*/", lines)
  3712. config = self.repo.get_config()
  3713. self.assertEqual(config.get((b"core",), b"sparseCheckout"), b"true")
  3714. self.assertEqual(config.get((b"core",), b"sparseCheckoutCone"), b"true")
  3715. def test_set_specific_dirs(self):
  3716. """Verify that cone_mode_set overwrites the included directories to only the specified ones.
  3717. Initializes cone mode, commits some files, then calls cone_mode_set with
  3718. a list of directories. Expects that only those directories remain in the
  3719. working tree.
  3720. """
  3721. porcelain.cone_mode_init(self.repo)
  3722. self._commit_file("docs/readme.md", b"# doc\n")
  3723. self._commit_file("src/main.py", b"print('hello')\n")
  3724. self._commit_file("tests/test_foo.py", b"# tests\n")
  3725. # Everything is still excluded initially by init.
  3726. porcelain.cone_mode_set(self.repo, dirs=["docs", "src"])
  3727. actual_files = self._list_wtree_files()
  3728. expected_files = {
  3729. os.path.join("docs", "readme.md"),
  3730. os.path.join("src", "main.py"),
  3731. }
  3732. self.assertEqual(
  3733. expected_files,
  3734. actual_files,
  3735. "Did not see only the 'docs/' and 'src/' dirs in the working tree.",
  3736. )
  3737. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  3738. with open(sp_path) as f:
  3739. lines = [ln.strip() for ln in f if ln.strip()]
  3740. # For standard cone mode, we'd expect lines like:
  3741. # /* (include top-level files)
  3742. # !/*/ (exclude subdirectories)
  3743. # !/docs/ (re-include docs)
  3744. # !/src/ (re-include src)
  3745. # Instead of the wildcard-based lines the old test used.
  3746. self.assertIn("/*", lines)
  3747. self.assertIn("!/*/", lines)
  3748. self.assertIn("/docs/", lines)
  3749. self.assertIn("/src/", lines)
  3750. self.assertNotIn("/tests/", lines)
  3751. def test_set_overwrites_old_dirs(self):
  3752. """Ensure that calling cone_mode_set again overwrites old includes.
  3753. Initializes cone mode, includes two directories, then calls
  3754. cone_mode_set again with a different directory to confirm the
  3755. new set of includes replaces the old.
  3756. """
  3757. porcelain.cone_mode_init(self.repo)
  3758. self._commit_file("docs/readme.md")
  3759. self._commit_file("src/main.py")
  3760. self._commit_file("tests/test_bar.py")
  3761. porcelain.cone_mode_set(self.repo, dirs=["docs", "src"])
  3762. self.assertEqual(
  3763. {os.path.join("docs", "readme.md"), os.path.join("src", "main.py")},
  3764. self._list_wtree_files(),
  3765. )
  3766. # Overwrite includes, now only 'tests'
  3767. porcelain.cone_mode_set(self.repo, dirs=["tests"], force=True)
  3768. actual_files = self._list_wtree_files()
  3769. expected_files = {os.path.join("tests", "test_bar.py")}
  3770. self.assertEqual(expected_files, actual_files)
  3771. def test_force_removal_of_local_mods(self):
  3772. """Confirm that force=True removes local changes in excluded paths.
  3773. cone_mode_init and cone_mode_set are called, a file is locally modified,
  3774. and then cone_mode_set is called again with force=True to exclude that path.
  3775. The excluded file should be removed with no CheckoutError.
  3776. """
  3777. porcelain.cone_mode_init(self.repo)
  3778. porcelain.cone_mode_set(self.repo, dirs=["docs"])
  3779. self._commit_file("docs/readme.md", b"Docs stuff\n")
  3780. self._commit_file("src/main.py", b"print('hello')\n")
  3781. # Modify src/main.py
  3782. with open(os.path.join(self.repo_path, "src/main.py"), "ab") as f:
  3783. f.write(b"extra line\n")
  3784. # Exclude src/ with force=True
  3785. porcelain.cone_mode_set(self.repo, dirs=["docs"], force=True)
  3786. actual_files = self._list_wtree_files()
  3787. expected_files = {os.path.join("docs", "readme.md")}
  3788. self.assertEqual(expected_files, actual_files)
  3789. def test_add_and_merge_dirs(self):
  3790. """Verify that cone_mode_add merges new directories instead of overwriting them.
  3791. After initializing cone mode and including a single directory, call
  3792. cone_mode_add with a new directory. Confirm that both directories
  3793. remain included. Repeat for an additional directory to ensure it
  3794. is merged, not overwritten.
  3795. """
  3796. porcelain.cone_mode_init(self.repo)
  3797. self._commit_file("docs/readme.md", b"# doc\n")
  3798. self._commit_file("src/main.py", b"print('hello')\n")
  3799. self._commit_file("tests/test_bar.py", b"# tests\n")
  3800. # Include "docs" only
  3801. porcelain.cone_mode_set(self.repo, dirs=["docs"])
  3802. self.assertEqual({os.path.join("docs", "readme.md")}, self._list_wtree_files())
  3803. # Add "src"
  3804. porcelain.cone_mode_add(self.repo, dirs=["src"])
  3805. actual_files = self._list_wtree_files()
  3806. self.assertEqual(
  3807. {os.path.join("docs", "readme.md"), os.path.join("src", "main.py")},
  3808. actual_files,
  3809. )
  3810. # Add "tests" as well
  3811. porcelain.cone_mode_add(self.repo, dirs=["tests"])
  3812. actual_files = self._list_wtree_files()
  3813. expected_files = {
  3814. os.path.join("docs", "readme.md"),
  3815. os.path.join("src", "main.py"),
  3816. os.path.join("tests", "test_bar.py"),
  3817. }
  3818. self.assertEqual(expected_files, actual_files)
  3819. # Check .git/info/sparse-checkout
  3820. sp_path = os.path.join(self.repo_path, ".git", "info", "sparse-checkout")
  3821. with open(sp_path) as f:
  3822. lines = [ln.strip() for ln in f if ln.strip()]
  3823. # Standard cone mode lines:
  3824. # "/*" -> include top-level
  3825. # "!/*/" -> exclude subdirectories
  3826. # "!/docs/", "!/src/", "!/tests/" -> re-include the directories we added
  3827. self.assertIn("/*", lines)
  3828. self.assertIn("!/*/", lines)
  3829. self.assertIn("/docs/", lines)
  3830. self.assertIn("/src/", lines)
  3831. self.assertIn("/tests/", lines)