2
0

test_cli.py 163 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433
  1. #!/usr/bin/env python
  2. # test_cli.py -- tests for dulwich.cli
  3. # vim: expandtab
  4. #
  5. # Copyright (C) 2024 Jelmer Vernooij <jelmer@jelmer.uk>
  6. #
  7. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  8. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  9. # General Public License as published by the Free Software Foundation; version 2.0
  10. # or (at your option) any later version. You can redistribute it and/or
  11. # modify it under the terms of either of these two licenses.
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. #
  19. # You should have received a copy of the licenses; if not, see
  20. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  21. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  22. # License, Version 2.0.
  23. """Tests for dulwich.cli."""
  24. import io
  25. import logging
  26. import os
  27. import shutil
  28. import sys
  29. import tempfile
  30. import unittest
  31. from unittest import skipIf
  32. from unittest.mock import MagicMock, patch
  33. from dulwich import cli
  34. from dulwich.cli import (
  35. AutoFlushBinaryIOWrapper,
  36. AutoFlushTextIOWrapper,
  37. _should_auto_flush,
  38. detect_terminal_width,
  39. format_bytes,
  40. launch_editor,
  41. write_columns,
  42. )
  43. from dulwich.repo import Repo
  44. from dulwich.tests.utils import (
  45. build_commit_graph,
  46. )
  47. from .. import TestCase
  48. class DulwichCliTestCase(TestCase):
  49. """Base class for CLI tests."""
  50. def setUp(self) -> None:
  51. super().setUp()
  52. # Suppress expected error logging during CLI tests
  53. cli_logger = logging.getLogger("dulwich.cli")
  54. original_cli_level = cli_logger.level
  55. cli_logger.setLevel(logging.CRITICAL)
  56. self.addCleanup(cli_logger.setLevel, original_cli_level)
  57. root_logger = logging.getLogger()
  58. original_root_level = root_logger.level
  59. root_logger.setLevel(logging.CRITICAL)
  60. self.addCleanup(root_logger.setLevel, original_root_level)
  61. self.test_dir = tempfile.mkdtemp()
  62. self.addCleanup(shutil.rmtree, self.test_dir)
  63. self.repo_path = os.path.join(self.test_dir, "repo")
  64. os.mkdir(self.repo_path)
  65. self.repo = Repo.init(self.repo_path)
  66. self.addCleanup(self.repo.close)
  67. def _run_cli(self, *args, stdout_stream=None):
  68. """Run CLI command and capture output."""
  69. class MockStream:
  70. def __init__(self):
  71. self._buffer = io.BytesIO()
  72. self.buffer = self._buffer
  73. def write(self, data):
  74. if isinstance(data, bytes):
  75. self._buffer.write(data)
  76. else:
  77. self._buffer.write(data.encode("utf-8"))
  78. def getvalue(self):
  79. value = self._buffer.getvalue()
  80. try:
  81. return value.decode("utf-8")
  82. except UnicodeDecodeError:
  83. return value
  84. def __getattr__(self, name):
  85. return getattr(self._buffer, name)
  86. old_stdout = sys.stdout
  87. old_stderr = sys.stderr
  88. old_cwd = os.getcwd()
  89. try:
  90. # Use custom stdout_stream if provided, otherwise use MockStream
  91. if stdout_stream:
  92. sys.stdout = stdout_stream
  93. if not hasattr(sys.stdout, "buffer"):
  94. sys.stdout.buffer = sys.stdout
  95. else:
  96. sys.stdout = MockStream()
  97. sys.stderr = MockStream()
  98. os.chdir(self.repo_path)
  99. result = cli.main(list(args))
  100. return result, sys.stdout.getvalue(), sys.stderr.getvalue()
  101. finally:
  102. sys.stdout = old_stdout
  103. sys.stderr = old_stderr
  104. os.chdir(old_cwd)
  105. class InitCommandTest(DulwichCliTestCase):
  106. """Tests for init command."""
  107. def test_init_basic(self):
  108. # Create a new directory for init
  109. new_repo_path = os.path.join(self.test_dir, "new_repo")
  110. _result, _stdout, _stderr = self._run_cli("init", new_repo_path)
  111. self.assertTrue(os.path.exists(os.path.join(new_repo_path, ".git")))
  112. def test_init_bare(self):
  113. # Create a new directory for bare repo
  114. bare_repo_path = os.path.join(self.test_dir, "bare_repo")
  115. _result, _stdout, _stderr = self._run_cli("init", "--bare", bare_repo_path)
  116. self.assertTrue(os.path.exists(os.path.join(bare_repo_path, "HEAD")))
  117. self.assertFalse(os.path.exists(os.path.join(bare_repo_path, ".git")))
  118. def test_init_objectformat_sha256(self) -> None:
  119. # Create a new directory for init with SHA-256
  120. new_repo_path = os.path.join(self.test_dir, "sha256_repo")
  121. _result, _stdout, _stderr = self._run_cli(
  122. "init", "--objectformat=sha256", new_repo_path
  123. )
  124. self.assertTrue(os.path.exists(os.path.join(new_repo_path, ".git")))
  125. # Verify the object format
  126. repo = Repo(new_repo_path)
  127. self.addCleanup(repo.close)
  128. config = repo.get_config()
  129. self.assertEqual(b"sha256", config.get((b"extensions",), b"objectformat"))
  130. def test_init_objectformat_sha1(self) -> None:
  131. # Create a new directory for init with SHA-1
  132. new_repo_path = os.path.join(self.test_dir, "sha1_repo")
  133. _result, _stdout, _stderr = self._run_cli(
  134. "init", "--objectformat=sha1", new_repo_path
  135. )
  136. self.assertTrue(os.path.exists(os.path.join(new_repo_path, ".git")))
  137. # SHA-1 is the default, so objectformat should not be set
  138. repo = Repo(new_repo_path)
  139. self.addCleanup(repo.close)
  140. config = repo.get_config()
  141. # The extensions section may not exist at all for SHA-1
  142. if config.has_section((b"extensions",)):
  143. object_format = config.get((b"extensions",), b"objectformat")
  144. self.assertNotEqual(b"sha256", object_format)
  145. # If the section doesn't exist, that's also fine (SHA-1 is default)
  146. def test_init_bare_objectformat_sha256(self) -> None:
  147. # Create a bare repo with SHA-256
  148. bare_repo_path = os.path.join(self.test_dir, "bare_sha256_repo")
  149. _result, _stdout, _stderr = self._run_cli(
  150. "init", "--bare", "--objectformat=sha256", bare_repo_path
  151. )
  152. self.assertTrue(os.path.exists(os.path.join(bare_repo_path, "HEAD")))
  153. self.assertFalse(os.path.exists(os.path.join(bare_repo_path, ".git")))
  154. # Verify the object format
  155. repo = Repo(bare_repo_path)
  156. self.addCleanup(repo.close)
  157. config = repo.get_config()
  158. self.assertEqual(b"sha256", config.get((b"extensions",), b"objectformat"))
  159. class HelperFunctionsTest(TestCase):
  160. """Tests for CLI helper functions."""
  161. def test_format_bytes(self):
  162. self.assertEqual("0.0 B", format_bytes(0))
  163. self.assertEqual("100.0 B", format_bytes(100))
  164. self.assertEqual("1.0 KB", format_bytes(1024))
  165. self.assertEqual("1.5 KB", format_bytes(1536))
  166. self.assertEqual("1.0 MB", format_bytes(1024 * 1024))
  167. self.assertEqual("1.0 GB", format_bytes(1024 * 1024 * 1024))
  168. self.assertEqual("1.0 TB", format_bytes(1024 * 1024 * 1024 * 1024))
  169. def test_launch_editor_with_cat(self):
  170. """Test launch_editor by using cat as the editor."""
  171. self.overrideEnv("GIT_EDITOR", "cat")
  172. result = launch_editor(b"Test template content")
  173. self.assertEqual(b"Test template content", result)
  174. def test_parse_time_to_timestamp(self):
  175. """Test parsing time specifications to Unix timestamps."""
  176. import time
  177. from dulwich.cli import parse_time_to_timestamp
  178. # Test special values
  179. self.assertEqual(0, parse_time_to_timestamp("never"))
  180. future_time = parse_time_to_timestamp("all")
  181. self.assertGreater(future_time, int(time.time()))
  182. # Test Unix timestamp
  183. self.assertEqual(1234567890, parse_time_to_timestamp("1234567890"))
  184. # Test relative time
  185. now = int(time.time())
  186. result = parse_time_to_timestamp("1 day ago")
  187. expected = now - 86400
  188. # Allow 2 second tolerance for test execution time
  189. self.assertAlmostEqual(expected, result, delta=2)
  190. class AddCommandTest(DulwichCliTestCase):
  191. """Tests for add command."""
  192. def test_add_single_file(self):
  193. # Create a file to add
  194. test_file = os.path.join(self.repo_path, "test.txt")
  195. with open(test_file, "w") as f:
  196. f.write("test content")
  197. _result, _stdout, _stderr = self._run_cli("add", "test.txt")
  198. # Check that file is in index
  199. self.assertIn(b"test.txt", self.repo.open_index())
  200. def test_add_multiple_files(self):
  201. # Create multiple files
  202. for i in range(3):
  203. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  204. with open(test_file, "w") as f:
  205. f.write(f"content {i}")
  206. _result, _stdout, _stderr = self._run_cli(
  207. "add", "test0.txt", "test1.txt", "test2.txt"
  208. )
  209. index = self.repo.open_index()
  210. self.assertIn(b"test0.txt", index)
  211. self.assertIn(b"test1.txt", index)
  212. self.assertIn(b"test2.txt", index)
  213. class RmCommandTest(DulwichCliTestCase):
  214. """Tests for rm command."""
  215. def test_rm_file(self):
  216. # Create, add and commit a file first
  217. test_file = os.path.join(self.repo_path, "test.txt")
  218. with open(test_file, "w") as f:
  219. f.write("test content")
  220. self._run_cli("add", "test.txt")
  221. self._run_cli("commit", "--message=Add test file")
  222. # Now remove it from index and working directory
  223. _result, _stdout, _stderr = self._run_cli("rm", "test.txt")
  224. # Check that file is not in index
  225. self.assertNotIn(b"test.txt", self.repo.open_index())
  226. class CommitCommandTest(DulwichCliTestCase):
  227. """Tests for commit command."""
  228. def test_commit_basic(self):
  229. # Create and add a file
  230. test_file = os.path.join(self.repo_path, "test.txt")
  231. with open(test_file, "w") as f:
  232. f.write("test content")
  233. self._run_cli("add", "test.txt")
  234. # Commit
  235. _result, _stdout, _stderr = self._run_cli("commit", "--message=Initial commit")
  236. # Check that HEAD points to a commit
  237. self.assertIsNotNone(self.repo.head())
  238. def test_commit_all_flag(self):
  239. # Create initial commit
  240. test_file = os.path.join(self.repo_path, "test.txt")
  241. with open(test_file, "w") as f:
  242. f.write("initial content")
  243. self._run_cli("add", "test.txt")
  244. self._run_cli("commit", "--message=Initial commit")
  245. # Modify the file (don't stage it)
  246. with open(test_file, "w") as f:
  247. f.write("modified content")
  248. # Create another file and don't add it (untracked)
  249. untracked_file = os.path.join(self.repo_path, "untracked.txt")
  250. with open(untracked_file, "w") as f:
  251. f.write("untracked content")
  252. # Commit with -a flag should stage and commit the modified file,
  253. # but not the untracked file
  254. _result, _stdout, _stderr = self._run_cli(
  255. "commit", "-a", "--message=Modified commit"
  256. )
  257. self.assertIsNotNone(self.repo.head())
  258. # Check that the modification was committed
  259. with open(test_file) as f:
  260. content = f.read()
  261. self.assertEqual(content, "modified content")
  262. # Check that untracked file is still untracked
  263. self.assertTrue(os.path.exists(untracked_file))
  264. def test_commit_all_flag_no_changes(self):
  265. # Create initial commit
  266. test_file = os.path.join(self.repo_path, "test.txt")
  267. with open(test_file, "w") as f:
  268. f.write("initial content")
  269. self._run_cli("add", "test.txt")
  270. self._run_cli("commit", "--message=Initial commit")
  271. # Try to commit with -a when there are no changes
  272. # This should still work (git allows this)
  273. _result, _stdout, _stderr = self._run_cli(
  274. "commit", "-a", "--message=No changes commit"
  275. )
  276. self.assertIsNotNone(self.repo.head())
  277. def test_commit_all_flag_multiple_files(self):
  278. # Create initial commit with multiple files
  279. file1 = os.path.join(self.repo_path, "file1.txt")
  280. file2 = os.path.join(self.repo_path, "file2.txt")
  281. with open(file1, "w") as f:
  282. f.write("content1")
  283. with open(file2, "w") as f:
  284. f.write("content2")
  285. self._run_cli("add", "file1.txt", "file2.txt")
  286. self._run_cli("commit", "--message=Initial commit")
  287. # Modify both files
  288. with open(file1, "w") as f:
  289. f.write("modified content1")
  290. with open(file2, "w") as f:
  291. f.write("modified content2")
  292. # Create an untracked file
  293. untracked_file = os.path.join(self.repo_path, "untracked.txt")
  294. with open(untracked_file, "w") as f:
  295. f.write("untracked content")
  296. # Commit with -a should stage both modified files but not untracked
  297. _result, _stdout, _stderr = self._run_cli(
  298. "commit", "-a", "--message=Modified both files"
  299. )
  300. self.assertIsNotNone(self.repo.head())
  301. # Verify modifications were committed
  302. with open(file1) as f:
  303. self.assertEqual(f.read(), "modified content1")
  304. with open(file2) as f:
  305. self.assertEqual(f.read(), "modified content2")
  306. # Verify untracked file still exists
  307. self.assertTrue(os.path.exists(untracked_file))
  308. @patch("dulwich.cli.launch_editor")
  309. def test_commit_editor_success(self, mock_editor):
  310. """Test commit with editor when user provides a message."""
  311. # Create and add a file
  312. test_file = os.path.join(self.repo_path, "test.txt")
  313. with open(test_file, "w") as f:
  314. f.write("test content")
  315. self._run_cli("add", "test.txt")
  316. # Mock editor to return a commit message
  317. mock_editor.return_value = b"My commit message\n\n# This is a comment\n"
  318. # Commit without --message flag
  319. _result, _stdout, _stderr = self._run_cli("commit")
  320. # Check that HEAD points to a commit
  321. commit = self.repo[self.repo.head()]
  322. self.assertEqual(commit.message, b"My commit message")
  323. # Verify editor was called
  324. mock_editor.assert_called_once()
  325. @patch("dulwich.cli.launch_editor")
  326. def test_commit_editor_empty_message(self, mock_editor):
  327. """Test commit with editor when user provides empty message."""
  328. # Create and add a file
  329. test_file = os.path.join(self.repo_path, "test.txt")
  330. with open(test_file, "w") as f:
  331. f.write("test content")
  332. self._run_cli("add", "test.txt")
  333. # Mock editor to return only comments
  334. mock_editor.return_value = b"# All lines are comments\n# No actual message\n"
  335. # Commit without --message flag should fail with exit code 1
  336. result, _stdout, _stderr = self._run_cli("commit")
  337. self.assertEqual(result, 1)
  338. @patch("dulwich.cli.launch_editor")
  339. def test_commit_editor_unchanged_template(self, mock_editor):
  340. """Test commit with editor when user doesn't change the template."""
  341. # Create and add a file
  342. test_file = os.path.join(self.repo_path, "test.txt")
  343. with open(test_file, "w") as f:
  344. f.write("test content")
  345. self._run_cli("add", "test.txt")
  346. # Mock editor to return the exact template that was passed to it
  347. def return_unchanged_template(template):
  348. return template
  349. mock_editor.side_effect = return_unchanged_template
  350. # Commit without --message flag should fail with exit code 1
  351. result, _stdout, _stderr = self._run_cli("commit")
  352. self.assertEqual(result, 1)
  353. class LogCommandTest(DulwichCliTestCase):
  354. """Tests for log command."""
  355. def test_log_empty_repo(self):
  356. _result, _stdout, _stderr = self._run_cli("log")
  357. # Empty repo should not crash
  358. def test_log_with_commits(self):
  359. # Create some commits
  360. _c1, _c2, c3 = build_commit_graph(
  361. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  362. )
  363. self.repo.refs[b"HEAD"] = c3.id
  364. _result, stdout, _stderr = self._run_cli("log")
  365. self.assertIn("Commit 3", stdout)
  366. self.assertIn("Commit 2", stdout)
  367. self.assertIn("Commit 1", stdout)
  368. def test_log_reverse(self):
  369. # Create some commits
  370. _c1, _c2, c3 = build_commit_graph(
  371. self.repo.object_store, [[1], [2, 1], [3, 1, 2]]
  372. )
  373. self.repo.refs[b"HEAD"] = c3.id
  374. _result, stdout, _stderr = self._run_cli("log", "--reverse")
  375. # Check order - commit 1 should appear before commit 3
  376. pos1 = stdout.index("Commit 1")
  377. pos3 = stdout.index("Commit 3")
  378. self.assertLess(pos1, pos3)
  379. class StatusCommandTest(DulwichCliTestCase):
  380. """Tests for status command."""
  381. def test_status_empty(self):
  382. _result, _stdout, _stderr = self._run_cli("status")
  383. # Should not crash on empty repo
  384. def test_status_with_untracked(self):
  385. # Create an untracked file
  386. test_file = os.path.join(self.repo_path, "untracked.txt")
  387. with open(test_file, "w") as f:
  388. f.write("untracked content")
  389. _result, stdout, _stderr = self._run_cli("status")
  390. self.assertIn("Untracked files:", stdout)
  391. self.assertIn("untracked.txt", stdout)
  392. def test_status_with_column(self):
  393. # Create multiple untracked files
  394. for i in range(5):
  395. test_file = os.path.join(self.repo_path, f"file{i}.txt")
  396. with open(test_file, "w") as f:
  397. f.write(f"content {i}")
  398. _result, stdout, _stderr = self._run_cli("status", "--column")
  399. self.assertIn("Untracked files:", stdout)
  400. # Check that files are present in output
  401. self.assertIn("file0.txt", stdout)
  402. self.assertIn("file1.txt", stdout)
  403. # With column format, multiple files should appear on same line
  404. # (at least for 5 short filenames)
  405. lines = stdout.split("\n")
  406. untracked_section = False
  407. for line in lines:
  408. if "Untracked files:" in line:
  409. untracked_section = True
  410. if untracked_section and "file" in line:
  411. # At least one line should contain multiple files
  412. if line.count("file") > 1:
  413. return # Test passes
  414. # If we get here and have multiple files, column formatting worked
  415. # (even if each is on its own line due to terminal width)
  416. class BranchCommandTest(DulwichCliTestCase):
  417. """Tests for branch command."""
  418. def test_branch_create(self):
  419. # Create initial commit
  420. test_file = os.path.join(self.repo_path, "test.txt")
  421. with open(test_file, "w") as f:
  422. f.write("test")
  423. self._run_cli("add", "test.txt")
  424. self._run_cli("commit", "--message=Initial")
  425. # Create branch
  426. _result, _stdout, _stderr = self._run_cli("branch", "test-branch")
  427. self.assertIn(b"refs/heads/test-branch", self.repo.refs.keys())
  428. def test_branch_delete(self):
  429. # Create initial commit and branch
  430. test_file = os.path.join(self.repo_path, "test.txt")
  431. with open(test_file, "w") as f:
  432. f.write("test")
  433. self._run_cli("add", "test.txt")
  434. self._run_cli("commit", "--message=Initial")
  435. self._run_cli("branch", "test-branch")
  436. # Delete branch
  437. _result, _stdout, _stderr = self._run_cli("branch", "-d", "test-branch")
  438. self.assertNotIn(b"refs/heads/test-branch", self.repo.refs.keys())
  439. def test_branch_list_all(self):
  440. # Create initial commit
  441. test_file = os.path.join(self.repo_path, "test.txt")
  442. with open(test_file, "w") as f:
  443. f.write("test")
  444. self._run_cli("add", "test.txt")
  445. self._run_cli("commit", "--message=Initial")
  446. # Create local test branches
  447. self._run_cli("branch", "feature-1")
  448. self._run_cli("branch", "feature-2")
  449. # Setup a remote and create remote branches
  450. self.repo.refs[b"refs/remotes/origin/master"] = self.repo.refs[
  451. b"refs/heads/master"
  452. ]
  453. self.repo.refs[b"refs/remotes/origin/feature-remote"] = self.repo.refs[
  454. b"refs/heads/master"
  455. ]
  456. # Test --all listing
  457. result, stdout, _stderr = self._run_cli("branch", "--all")
  458. self.assertEqual(result, 0)
  459. expected_branches = {
  460. "feature-1", # local branch
  461. "feature-2", # local branch
  462. "master", # local branch
  463. "origin/master", # remote branch
  464. "origin/feature-remote", # remote branch
  465. }
  466. lines = [line.strip() for line in stdout.splitlines()]
  467. # All branches from stdout
  468. all_branches = set(line for line in lines)
  469. self.assertEqual(all_branches, expected_branches)
  470. def test_branch_list_merged(self):
  471. # Create initial commit
  472. test_file = os.path.join(self.repo_path, "test.txt")
  473. with open(test_file, "w") as f:
  474. f.write("test")
  475. self._run_cli("add", "test.txt")
  476. self._run_cli("commit", "--message=Initial")
  477. master_sha = self.repo.refs[b"refs/heads/master"]
  478. # Create a merged branch (points to same commit as master)
  479. self.repo.refs[b"refs/heads/merged-branch"] = master_sha
  480. # Create a new branch with different content (not merged)
  481. test_file2 = os.path.join(self.repo_path, "test2.txt")
  482. with open(test_file2, "w") as f:
  483. f.write("test2")
  484. self._run_cli("add", "test2.txt")
  485. self._run_cli("commit", "--message=New branch commit")
  486. new_branch_sha = self.repo.refs[b"HEAD"]
  487. # Switch back to master
  488. self.repo.refs[b"HEAD"] = master_sha
  489. # Create a non-merged branch that points to the new branch commit
  490. self.repo.refs[b"refs/heads/non-merged-branch"] = new_branch_sha
  491. # Test --merged listing
  492. result, stdout, _stderr = self._run_cli("branch", "--merged")
  493. self.assertEqual(result, 0)
  494. branches = [line.strip() for line in stdout.splitlines()]
  495. expected_branches = {"master", "merged-branch"}
  496. self.assertEqual(set(branches), expected_branches)
  497. def test_branch_list_no_merged(self):
  498. # Create initial commit
  499. test_file = os.path.join(self.repo_path, "test.txt")
  500. with open(test_file, "w") as f:
  501. f.write("test")
  502. self._run_cli("add", "test.txt")
  503. self._run_cli("commit", "--message=Initial")
  504. master_sha = self.repo.refs[b"refs/heads/master"]
  505. # Create a merged branch (points to same commit as master)
  506. self.repo.refs[b"refs/heads/merged-branch"] = master_sha
  507. # Create a new branch with different content (not merged)
  508. test_file2 = os.path.join(self.repo_path, "test2.txt")
  509. with open(test_file2, "w") as f:
  510. f.write("test2")
  511. self._run_cli("add", "test2.txt")
  512. self._run_cli("commit", "--message=New branch commit")
  513. new_branch_sha = self.repo.refs[b"HEAD"]
  514. # Switch back to master
  515. self.repo.refs[b"HEAD"] = master_sha
  516. # Create a non-merged branch that points to the new branch commit
  517. self.repo.refs[b"refs/heads/non-merged-branch"] = new_branch_sha
  518. # Test --no-merged listing
  519. result, stdout, _stderr = self._run_cli("branch", "--no-merged")
  520. self.assertEqual(result, 0)
  521. branches = [line.strip() for line in stdout.splitlines()]
  522. expected_branches = {"non-merged-branch"}
  523. self.assertEqual(set(branches), expected_branches)
  524. def test_branch_list_remotes(self):
  525. # Create initial commit
  526. test_file = os.path.join(self.repo_path, "test.txt")
  527. with open(test_file, "w") as f:
  528. f.write("test")
  529. self._run_cli("add", "test.txt")
  530. self._run_cli("commit", "--message=Initial")
  531. # Setup a remote and create remote branches
  532. self.repo.refs[b"refs/remotes/origin/master"] = self.repo.refs[
  533. b"refs/heads/master"
  534. ]
  535. self.repo.refs[b"refs/remotes/origin/feature-remote-1"] = self.repo.refs[
  536. b"refs/heads/master"
  537. ]
  538. self.repo.refs[b"refs/remotes/origin/feature-remote-2"] = self.repo.refs[
  539. b"refs/heads/master"
  540. ]
  541. # Test --remotes listing
  542. result, stdout, _stderr = self._run_cli("branch", "--remotes")
  543. self.assertEqual(result, 0)
  544. branches = [line.strip() for line in stdout.splitlines()]
  545. expected_branches = [
  546. "origin/feature-remote-1",
  547. "origin/feature-remote-2",
  548. "origin/master",
  549. ]
  550. self.assertEqual(branches, expected_branches)
  551. def test_branch_list_contains(self):
  552. # Create initial commit
  553. test_file = os.path.join(self.repo_path, "test.txt")
  554. with open(test_file, "w") as f:
  555. f.write("test")
  556. self._run_cli("add", "test.txt")
  557. self._run_cli("commit", "--message=Initial")
  558. initial_commit_sha = self.repo.refs[b"HEAD"]
  559. # Create first branch from initial commit
  560. self._run_cli("branch", "branch-1")
  561. # Make a new commit on master
  562. test_file2 = os.path.join(self.repo_path, "test2.txt")
  563. with open(test_file2, "w") as f:
  564. f.write("test2")
  565. self._run_cli("add", "test2.txt")
  566. self._run_cli("commit", "--message=Second commit")
  567. second_commit_sha = self.repo.refs[b"HEAD"]
  568. # Create second branch from current master (contains both commits)
  569. self._run_cli("branch", "branch-2")
  570. # Create third branch that doesn't contain the second commit
  571. # Switch to initial commit and create branch from there
  572. self.repo.refs[b"HEAD"] = initial_commit_sha
  573. self._run_cli("branch", "branch-3")
  574. # Switch back to master
  575. self.repo.refs[b"HEAD"] = second_commit_sha
  576. # Test --contains with second commit (should include master and branch-2)
  577. result, stdout, stderr = self._run_cli(
  578. "branch", "--contains", second_commit_sha.decode()
  579. )
  580. self.assertEqual(result, 0)
  581. branches = [line.strip() for line in stdout.splitlines()]
  582. expected_branches = {"master", "branch-2"}
  583. self.assertEqual(set(branches), expected_branches)
  584. # Test --contains with initial commit (should include all branches)
  585. result, stdout, stderr = self._run_cli(
  586. "branch", "--contains", initial_commit_sha.decode()
  587. )
  588. self.assertEqual(result, 0)
  589. branches = [line.strip() for line in stdout.splitlines()]
  590. expected_branches = {"master", "branch-1", "branch-2", "branch-3"}
  591. self.assertEqual(set(branches), expected_branches)
  592. # Test --contains without argument (uses HEAD, which is second commit)
  593. result, stdout, stderr = self._run_cli("branch", "--contains")
  594. self.assertEqual(result, 0)
  595. branches = [line.strip() for line in stdout.splitlines()]
  596. expected_branches = {"master", "branch-2"}
  597. self.assertEqual(set(branches), expected_branches)
  598. # Test with invalid commit hash
  599. result, stdout, stderr = self._run_cli("branch", "--contains", "invalid123")
  600. self.assertNotEqual(result, 0)
  601. self.assertIn("error: object name invalid123 not found", stderr)
  602. def test_branch_list_column(self):
  603. """Test branch --column formatting"""
  604. # Create initial commit
  605. test_file = os.path.join(self.repo_path, "test.txt")
  606. with open(test_file, "w") as f:
  607. f.write("test")
  608. self._run_cli("add", "test.txt")
  609. self._run_cli("commit", "--message=Initial")
  610. self._run_cli("branch", "feature-1")
  611. self._run_cli("branch", "feature-2")
  612. self._run_cli("branch", "feature-3")
  613. # Run branch --column
  614. result, stdout, _stderr = self._run_cli("branch", "--all", "--column")
  615. self.assertEqual(result, 0)
  616. expected = ["feature-1", "feature-2", "feature-3"]
  617. for branch in expected:
  618. self.assertIn(branch, stdout)
  619. multiple_columns = any(
  620. sum(branch in line for branch in expected) > 1
  621. for line in stdout.strip().split("\n")
  622. )
  623. self.assertTrue(multiple_columns)
  624. def test_branch_list_flag(self):
  625. # Create an initial commit
  626. test_file = os.path.join(self.repo_path, "test.txt")
  627. with open(test_file, "w") as f:
  628. f.write("test")
  629. self._run_cli("add", "test.txt")
  630. self._run_cli("commit", "--message=Initial")
  631. # Create local branches
  632. self._run_cli("branch", "feature-1")
  633. self._run_cli("branch", "feature-2")
  634. self._run_cli("branch", "branch-1")
  635. # Run `branch --list` with a pattern "feature-*"
  636. result, stdout, _stderr = self._run_cli(
  637. "branch", "--all", "--list", "feature-*"
  638. )
  639. self.assertEqual(result, 0)
  640. # Collect branches from the output
  641. branches = [line.strip() for line in stdout.splitlines()]
  642. # Expected branches — exactly those matching the pattern
  643. expected_branches = ["feature-1", "feature-2"]
  644. self.assertEqual(branches, expected_branches)
  645. class TestTerminalWidth(TestCase):
  646. @patch("os.get_terminal_size")
  647. def test_terminal_size(self, mock_get_terminal_size):
  648. """Test os.get_terminal_size mocking."""
  649. mock_get_terminal_size.return_value.columns = 100
  650. width = detect_terminal_width()
  651. self.assertEqual(width, 100)
  652. @patch("os.get_terminal_size")
  653. def test_terminal_size_os_error(self, mock_get_terminal_size):
  654. """Test os.get_terminal_size raising OSError."""
  655. mock_get_terminal_size.side_effect = OSError("No terminal")
  656. width = detect_terminal_width()
  657. self.assertEqual(width, 80)
  658. class TestWriteColumns(TestCase):
  659. """Tests for write_columns function"""
  660. def test_basic_functionality(self):
  661. """Test basic functionality with default terminal width."""
  662. out = io.StringIO()
  663. items = [b"main", b"dev", b"feature/branch-1"]
  664. write_columns(items, out, width=80)
  665. output_text = out.getvalue()
  666. self.assertEqual(output_text, "main dev feature/branch-1\n")
  667. def test_narrow_terminal_single_column(self):
  668. """Test with narrow terminal forcing single column."""
  669. out = io.StringIO()
  670. items = [b"main", b"dev", b"feature/branch-1"]
  671. write_columns(items, out, 20)
  672. self.assertEqual(out.getvalue(), "main\ndev\nfeature/branch-1\n")
  673. def test_wide_terminal_multiple_columns(self):
  674. """Test with wide terminal allowing multiple columns."""
  675. out = io.StringIO()
  676. items = [
  677. b"main",
  678. b"dev",
  679. b"feature/branch-1",
  680. b"feature/branch-2",
  681. b"feature/branch-3",
  682. ]
  683. write_columns(items, out, 120)
  684. output_text = out.getvalue()
  685. self.assertEqual(
  686. output_text,
  687. "main dev feature/branch-1 feature/branch-2 feature/branch-3\n",
  688. )
  689. def test_single_item(self):
  690. """Test with single item."""
  691. out = io.StringIO()
  692. write_columns([b"single"], out, 80)
  693. output_text = out.getvalue()
  694. self.assertEqual("single\n", output_text)
  695. self.assertTrue(output_text.endswith("\n"))
  696. def test_os_error_fallback(self):
  697. """Test fallback behavior when os.get_terminal_size raises OSError."""
  698. with patch("os.get_terminal_size", side_effect=OSError("No terminal")):
  699. out = io.StringIO()
  700. items = [b"main", b"dev"]
  701. write_columns(items, out)
  702. output_text = out.getvalue()
  703. # With default width (80), should display in columns
  704. self.assertEqual(output_text, "main dev\n")
  705. def test_iterator_input(self):
  706. """Test with iterator input instead of list."""
  707. out = io.StringIO()
  708. items = [b"main", b"dev", b"feature/branch-1"]
  709. items_iterator = iter(items)
  710. write_columns(items_iterator, out, 80)
  711. output_text = out.getvalue()
  712. self.assertEqual(output_text, "main dev feature/branch-1\n")
  713. def test_column_alignment(self):
  714. """Test that columns are properly aligned."""
  715. out = io.StringIO()
  716. items = [b"short", b"medium_length", b"very_long______name"]
  717. write_columns(items, out, 50)
  718. output_text = out.getvalue()
  719. self.assertEqual(output_text, "short medium_length very_long______name\n")
  720. def test_columns_formatting(self):
  721. """Test that items are formatted in columns within single line."""
  722. out = io.StringIO()
  723. items = [b"branch-1", b"branch-2", b"branch-3", b"branch-4", b"branch-5"]
  724. write_columns(items, out, 80)
  725. output_text = out.getvalue()
  726. self.assertEqual(output_text.count("\n"), 1)
  727. self.assertTrue(output_text.endswith("\n"))
  728. line = output_text.strip()
  729. for item in items:
  730. self.assertIn(item.decode(), line)
  731. def test_column_alignment_multiple_lines(self):
  732. """Test that columns are properly aligned across multiple lines."""
  733. items = [
  734. b"short",
  735. b"medium_length",
  736. b"very_long_branch_name",
  737. b"another",
  738. b"more",
  739. b"even_longer_branch_name_here",
  740. ]
  741. out = io.StringIO()
  742. write_columns(items, out, width=60)
  743. output_text = out.getvalue()
  744. lines = output_text.strip().split("\n")
  745. self.assertGreater(len(lines), 1)
  746. line_lengths = [len(line) for line in lines if line.strip()]
  747. for length in line_lengths:
  748. self.assertLessEqual(length, 60)
  749. all_output = " ".join(lines)
  750. for item in items:
  751. self.assertIn(item.decode(), all_output)
  752. class CheckoutCommandTest(DulwichCliTestCase):
  753. """Tests for checkout command."""
  754. def test_checkout_branch(self):
  755. # Create initial commit and branch
  756. test_file = os.path.join(self.repo_path, "test.txt")
  757. with open(test_file, "w") as f:
  758. f.write("test")
  759. self._run_cli("add", "test.txt")
  760. self._run_cli("commit", "--message=Initial")
  761. self._run_cli("branch", "test-branch")
  762. # Checkout branch
  763. _result, _stdout, _stderr = self._run_cli("checkout", "test-branch")
  764. self.assertEqual(
  765. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  766. )
  767. class TagCommandTest(DulwichCliTestCase):
  768. """Tests for tag command."""
  769. def test_tag_create(self):
  770. # Create initial commit
  771. test_file = os.path.join(self.repo_path, "test.txt")
  772. with open(test_file, "w") as f:
  773. f.write("test")
  774. self._run_cli("add", "test.txt")
  775. self._run_cli("commit", "--message=Initial")
  776. # Create tag
  777. _result, _stdout, _stderr = self._run_cli("tag", "v1.0")
  778. self.assertIn(b"refs/tags/v1.0", self.repo.refs.keys())
  779. class VerifyCommitCommandTest(DulwichCliTestCase):
  780. """Tests for verify-commit command."""
  781. def test_verify_commit_basic(self):
  782. # Create initial commit
  783. test_file = os.path.join(self.repo_path, "test.txt")
  784. with open(test_file, "w") as f:
  785. f.write("test")
  786. self._run_cli("add", "test.txt")
  787. self._run_cli("commit", "--message=Initial")
  788. # Mock the porcelain.verify_commit function since we don't have GPG setup
  789. with patch("dulwich.cli.porcelain.verify_commit") as mock_verify:
  790. _result, stdout, _stderr = self._run_cli("verify-commit", "HEAD")
  791. mock_verify.assert_called_once_with(".", "HEAD")
  792. self.assertIn("Good signature", stdout)
  793. def test_verify_commit_multiple(self):
  794. # Create multiple commits
  795. test_file = os.path.join(self.repo_path, "test.txt")
  796. with open(test_file, "w") as f:
  797. f.write("test1")
  798. self._run_cli("add", "test.txt")
  799. self._run_cli("commit", "--message=First")
  800. with open(test_file, "w") as f:
  801. f.write("test2")
  802. self._run_cli("add", "test.txt")
  803. self._run_cli("commit", "--message=Second")
  804. # Mock the porcelain.verify_commit function
  805. with patch("dulwich.cli.porcelain.verify_commit") as mock_verify:
  806. _result, stdout, _stderr = self._run_cli("verify-commit", "HEAD", "HEAD~1")
  807. self.assertEqual(mock_verify.call_count, 2)
  808. self.assertIn("HEAD", stdout)
  809. self.assertIn("HEAD~1", stdout)
  810. def test_verify_commit_default_head(self):
  811. # Create initial commit
  812. test_file = os.path.join(self.repo_path, "test.txt")
  813. with open(test_file, "w") as f:
  814. f.write("test")
  815. self._run_cli("add", "test.txt")
  816. self._run_cli("commit", "--message=Initial")
  817. # Mock the porcelain.verify_commit function
  818. with patch("dulwich.cli.porcelain.verify_commit") as mock_verify:
  819. # Test that verify-commit without arguments defaults to HEAD
  820. _result, stdout, _stderr = self._run_cli("verify-commit")
  821. mock_verify.assert_called_once_with(".", "HEAD")
  822. self.assertIn("Good signature", stdout)
  823. class VerifyTagCommandTest(DulwichCliTestCase):
  824. """Tests for verify-tag command."""
  825. def test_verify_tag_basic(self):
  826. # Create initial commit
  827. test_file = os.path.join(self.repo_path, "test.txt")
  828. with open(test_file, "w") as f:
  829. f.write("test")
  830. self._run_cli("add", "test.txt")
  831. self._run_cli("commit", "--message=Initial")
  832. # Create an annotated tag
  833. self._run_cli("tag", "--annotated", "v1.0")
  834. # Mock the porcelain.verify_tag function since we don't have GPG setup
  835. with patch("dulwich.cli.porcelain.verify_tag") as mock_verify:
  836. _result, stdout, _stderr = self._run_cli("verify-tag", "v1.0")
  837. mock_verify.assert_called_once_with(".", "v1.0")
  838. self.assertIn("Good signature", stdout)
  839. def test_verify_tag_multiple(self):
  840. # Create initial commit
  841. test_file = os.path.join(self.repo_path, "test.txt")
  842. with open(test_file, "w") as f:
  843. f.write("test")
  844. self._run_cli("add", "test.txt")
  845. self._run_cli("commit", "--message=Initial")
  846. # Create multiple annotated tags
  847. self._run_cli("tag", "--annotated", "v1.0")
  848. self._run_cli("tag", "--annotated", "v2.0")
  849. # Mock the porcelain.verify_tag function
  850. with patch("dulwich.cli.porcelain.verify_tag") as mock_verify:
  851. _result, stdout, _stderr = self._run_cli("verify-tag", "v1.0", "v2.0")
  852. self.assertEqual(mock_verify.call_count, 2)
  853. self.assertIn("v1.0", stdout)
  854. self.assertIn("v2.0", stdout)
  855. class DiffCommandTest(DulwichCliTestCase):
  856. """Tests for diff command."""
  857. def test_diff_working_tree(self):
  858. # Create and commit a file
  859. test_file = os.path.join(self.repo_path, "test.txt")
  860. with open(test_file, "w") as f:
  861. f.write("initial content\n")
  862. self._run_cli("add", "test.txt")
  863. self._run_cli("commit", "--message=Initial")
  864. # Modify the file
  865. with open(test_file, "w") as f:
  866. f.write("initial content\nmodified\n")
  867. # Test unstaged diff
  868. _result, stdout, _stderr = self._run_cli("diff")
  869. self.assertIn("+modified", stdout)
  870. def test_diff_staged(self):
  871. # Create initial commit
  872. test_file = os.path.join(self.repo_path, "test.txt")
  873. with open(test_file, "w") as f:
  874. f.write("initial content\n")
  875. self._run_cli("add", "test.txt")
  876. self._run_cli("commit", "--message=Initial")
  877. # Modify and stage the file
  878. with open(test_file, "w") as f:
  879. f.write("initial content\nnew file\n")
  880. self._run_cli("add", "test.txt")
  881. # Test staged diff
  882. _result, stdout, _stderr = self._run_cli("diff", "--staged")
  883. self.assertIn("+new file", stdout)
  884. def test_diff_cached(self):
  885. # Create initial commit
  886. test_file = os.path.join(self.repo_path, "test.txt")
  887. with open(test_file, "w") as f:
  888. f.write("initial content\n")
  889. self._run_cli("add", "test.txt")
  890. self._run_cli("commit", "--message=Initial")
  891. # Modify and stage the file
  892. with open(test_file, "w") as f:
  893. f.write("initial content\nnew file\n")
  894. self._run_cli("add", "test.txt")
  895. # Test cached diff (alias for staged)
  896. _result, stdout, _stderr = self._run_cli("diff", "--cached")
  897. self.assertIn("+new file", stdout)
  898. def test_diff_commit(self):
  899. # Create two commits
  900. test_file = os.path.join(self.repo_path, "test.txt")
  901. with open(test_file, "w") as f:
  902. f.write("first version\n")
  903. self._run_cli("add", "test.txt")
  904. self._run_cli("commit", "--message=First")
  905. with open(test_file, "w") as f:
  906. f.write("first version\nsecond line\n")
  907. self._run_cli("add", "test.txt")
  908. self._run_cli("commit", "--message=Second")
  909. # Add working tree changes
  910. with open(test_file, "a") as f:
  911. f.write("working tree change\n")
  912. # Test single commit diff (should show working tree vs HEAD)
  913. _result, stdout, _stderr = self._run_cli("diff", "HEAD")
  914. self.assertIn("+working tree change", stdout)
  915. def test_diff_two_commits(self):
  916. # Create two commits
  917. test_file = os.path.join(self.repo_path, "test.txt")
  918. with open(test_file, "w") as f:
  919. f.write("first version\n")
  920. self._run_cli("add", "test.txt")
  921. self._run_cli("commit", "--message=First")
  922. # Get first commit SHA
  923. first_commit = self.repo.refs[b"HEAD"].decode()
  924. with open(test_file, "w") as f:
  925. f.write("first version\nsecond line\n")
  926. self._run_cli("add", "test.txt")
  927. self._run_cli("commit", "--message=Second")
  928. # Get second commit SHA
  929. second_commit = self.repo.refs[b"HEAD"].decode()
  930. # Test diff between two commits
  931. _result, stdout, _stderr = self._run_cli("diff", first_commit, second_commit)
  932. self.assertIn("+second line", stdout)
  933. def test_diff_commit_vs_working_tree(self):
  934. # Test that diff <commit> shows working tree vs commit (not commit vs parent)
  935. test_file = os.path.join(self.repo_path, "test.txt")
  936. with open(test_file, "w") as f:
  937. f.write("first version\n")
  938. self._run_cli("add", "test.txt")
  939. self._run_cli("commit", "--message=First")
  940. first_commit = self.repo.refs[b"HEAD"].decode()
  941. with open(test_file, "w") as f:
  942. f.write("first version\nsecond line\n")
  943. self._run_cli("add", "test.txt")
  944. self._run_cli("commit", "--message=Second")
  945. # Add changes to working tree
  946. with open(test_file, "w") as f:
  947. f.write("completely different\n")
  948. # diff <first_commit> should show working tree vs first commit
  949. _result, stdout, _stderr = self._run_cli("diff", first_commit)
  950. self.assertIn("-first version", stdout)
  951. self.assertIn("+completely different", stdout)
  952. def test_diff_with_paths(self):
  953. # Test path filtering
  954. # Create multiple files
  955. file1 = os.path.join(self.repo_path, "file1.txt")
  956. file2 = os.path.join(self.repo_path, "file2.txt")
  957. subdir = os.path.join(self.repo_path, "subdir")
  958. os.makedirs(subdir)
  959. file3 = os.path.join(subdir, "file3.txt")
  960. with open(file1, "w") as f:
  961. f.write("content1\n")
  962. with open(file2, "w") as f:
  963. f.write("content2\n")
  964. with open(file3, "w") as f:
  965. f.write("content3\n")
  966. self._run_cli("add", ".")
  967. self._run_cli("commit", "--message=Initial")
  968. # Modify all files
  969. with open(file1, "w") as f:
  970. f.write("modified1\n")
  971. with open(file2, "w") as f:
  972. f.write("modified2\n")
  973. with open(file3, "w") as f:
  974. f.write("modified3\n")
  975. # Test diff with specific file
  976. _result, stdout, _stderr = self._run_cli("diff", "--", "file1.txt")
  977. self.assertIn("file1.txt", stdout)
  978. self.assertNotIn("file2.txt", stdout)
  979. self.assertNotIn("file3.txt", stdout)
  980. # Test diff with directory
  981. _result, stdout, _stderr = self._run_cli("diff", "--", "subdir")
  982. self.assertNotIn("file1.txt", stdout)
  983. self.assertNotIn("file2.txt", stdout)
  984. self.assertIn("file3.txt", stdout)
  985. # Test staged diff with paths
  986. self._run_cli("add", "file1.txt")
  987. _result, stdout, _stderr = self._run_cli("diff", "--staged", "--", "file1.txt")
  988. self.assertIn("file1.txt", stdout)
  989. self.assertIn("+modified1", stdout)
  990. # Test diff with multiple paths (file2 and file3 are still unstaged)
  991. _result, stdout, _stderr = self._run_cli(
  992. "diff", "--", "file2.txt", "subdir/file3.txt"
  993. )
  994. self.assertIn("file2.txt", stdout)
  995. self.assertIn("file3.txt", stdout)
  996. self.assertNotIn("file1.txt", stdout)
  997. # Test diff with commit and paths
  998. first_commit = self.repo.refs[b"HEAD"].decode()
  999. with open(file1, "w") as f:
  1000. f.write("newer1\n")
  1001. _result, stdout, _stderr = self._run_cli(
  1002. "diff", first_commit, "--", "file1.txt"
  1003. )
  1004. self.assertIn("file1.txt", stdout)
  1005. self.assertIn("-content1", stdout)
  1006. self.assertIn("+newer1", stdout)
  1007. self.assertNotIn("file2.txt", stdout)
  1008. def test_diff_stat(self):
  1009. # Create and commit a file
  1010. test_file = os.path.join(self.repo_path, "test.txt")
  1011. with open(test_file, "w") as f:
  1012. f.write("initial content\n")
  1013. self._run_cli("add", "test.txt")
  1014. self._run_cli("commit", "--message=Initial")
  1015. # Modify the file
  1016. with open(test_file, "w") as f:
  1017. f.write("initial content\nmodified\n")
  1018. # Test --stat output
  1019. _result, stdout, _stderr = self._run_cli("diff", "--stat")
  1020. self.assertEqual(
  1021. stdout,
  1022. " test.txt | 1 +\n 1 files changed, 1 insertions(+), 0 deletions(-)\n",
  1023. )
  1024. class FilterBranchCommandTest(DulwichCliTestCase):
  1025. """Tests for filter-branch command."""
  1026. def setUp(self):
  1027. super().setUp()
  1028. # Create a more complex repository structure for testing
  1029. # Create some files in subdirectories
  1030. os.makedirs(os.path.join(self.repo_path, "subdir"))
  1031. os.makedirs(os.path.join(self.repo_path, "other"))
  1032. # Create files
  1033. files = {
  1034. "README.md": "# Test Repo",
  1035. "subdir/file1.txt": "File in subdir",
  1036. "subdir/file2.txt": "Another file in subdir",
  1037. "other/file3.txt": "File in other dir",
  1038. "root.txt": "File at root",
  1039. }
  1040. for path, content in files.items():
  1041. file_path = os.path.join(self.repo_path, path)
  1042. with open(file_path, "w") as f:
  1043. f.write(content)
  1044. # Add all files and create initial commit
  1045. self._run_cli("add", ".")
  1046. self._run_cli("commit", "--message=Initial commit")
  1047. # Create a second commit modifying subdir
  1048. with open(os.path.join(self.repo_path, "subdir/file1.txt"), "a") as f:
  1049. f.write("\nModified content")
  1050. self._run_cli("add", "subdir/file1.txt")
  1051. self._run_cli("commit", "--message=Modify subdir file")
  1052. # Create a third commit in other dir
  1053. with open(os.path.join(self.repo_path, "other/file3.txt"), "a") as f:
  1054. f.write("\nMore content")
  1055. self._run_cli("add", "other/file3.txt")
  1056. self._run_cli("commit", "--message=Modify other file")
  1057. # Create a branch
  1058. self._run_cli("branch", "test-branch")
  1059. # Create a tag
  1060. self._run_cli("tag", "v1.0")
  1061. def test_filter_branch_subdirectory_filter(self):
  1062. """Test filter-branch with subdirectory filter."""
  1063. # Run filter-branch to extract only the subdir
  1064. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1065. result, _stdout, _stderr = self._run_cli(
  1066. "filter-branch", "--subdirectory-filter", "subdir"
  1067. )
  1068. # Check that the operation succeeded
  1069. self.assertEqual(result, 0)
  1070. log_output = "\n".join(cm.output)
  1071. self.assertIn("Rewrite HEAD", log_output)
  1072. # filter-branch rewrites history but doesn't update working tree
  1073. # We need to check the commit contents, not the working tree
  1074. # Reset to the rewritten HEAD to update working tree
  1075. self._run_cli("reset", "--hard", "HEAD")
  1076. # Now check that only files from subdir remain at root level
  1077. self.assertTrue(os.path.exists(os.path.join(self.repo_path, "file1.txt")))
  1078. self.assertTrue(os.path.exists(os.path.join(self.repo_path, "file2.txt")))
  1079. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "README.md")))
  1080. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "root.txt")))
  1081. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "other")))
  1082. self.assertFalse(os.path.exists(os.path.join(self.repo_path, "subdir")))
  1083. # Check that original refs were backed up
  1084. original_refs = [
  1085. ref for ref in self.repo.refs.keys() if ref.startswith(b"refs/original/")
  1086. ]
  1087. self.assertTrue(
  1088. len(original_refs) > 0, "No original refs found after filter-branch"
  1089. )
  1090. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  1091. def test_filter_branch_msg_filter(self):
  1092. """Test filter-branch with message filter."""
  1093. # Run filter-branch to prepend [FILTERED] to commit messages
  1094. result, stdout, _stderr = self._run_cli(
  1095. "filter-branch", "--msg-filter", "sed 's/^/[FILTERED] /'"
  1096. )
  1097. self.assertEqual(result, 0)
  1098. # Check that commit messages were modified
  1099. result, stdout, _stderr = self._run_cli("log")
  1100. self.assertIn("[FILTERED] Modify other file", stdout)
  1101. self.assertIn("[FILTERED] Modify subdir file", stdout)
  1102. self.assertIn("[FILTERED] Initial commit", stdout)
  1103. def test_filter_branch_env_filter(self):
  1104. """Test filter-branch with environment filter."""
  1105. # Run filter-branch to change author email
  1106. env_filter = """
  1107. if [ "$GIT_AUTHOR_EMAIL" = "test@example.com" ]; then
  1108. export GIT_AUTHOR_EMAIL="filtered@example.com"
  1109. fi
  1110. """
  1111. result, _stdout, _stderr = self._run_cli(
  1112. "filter-branch", "--env-filter", env_filter
  1113. )
  1114. self.assertEqual(result, 0)
  1115. def test_filter_branch_prune_empty(self):
  1116. """Test filter-branch with prune-empty option."""
  1117. # Create a commit that only touches files outside subdir
  1118. with open(os.path.join(self.repo_path, "root.txt"), "a") as f:
  1119. f.write("\nNew line")
  1120. self._run_cli("add", "root.txt")
  1121. self._run_cli("commit", "--message=Modify root file only")
  1122. # Run filter-branch to extract subdir with prune-empty
  1123. result, stdout, _stderr = self._run_cli(
  1124. "filter-branch", "--subdirectory-filter", "subdir", "--prune-empty"
  1125. )
  1126. self.assertEqual(result, 0)
  1127. # The last commit should have been pruned
  1128. result, stdout, _stderr = self._run_cli("log")
  1129. self.assertNotIn("Modify root file only", stdout)
  1130. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  1131. def test_filter_branch_force(self):
  1132. """Test filter-branch with force option."""
  1133. # Run filter-branch once with a filter that actually changes something
  1134. result, _stdout, _stderr = self._run_cli(
  1135. "filter-branch", "--msg-filter", "sed 's/^/[TEST] /'"
  1136. )
  1137. self.assertEqual(result, 0)
  1138. # Check that backup refs were created
  1139. # The implementation backs up refs under refs/original/
  1140. original_refs = [
  1141. ref for ref in self.repo.refs.keys() if ref.startswith(b"refs/original/")
  1142. ]
  1143. self.assertTrue(len(original_refs) > 0, "No original refs found")
  1144. # Run again without force - should fail
  1145. with self.assertLogs("dulwich.cli", level="ERROR") as cm:
  1146. result, _stdout, _stderr = self._run_cli(
  1147. "filter-branch", "--msg-filter", "sed 's/^/[TEST2] /'"
  1148. )
  1149. self.assertEqual(result, 1)
  1150. log_output = "\n".join(cm.output)
  1151. self.assertIn("Cannot create a new backup", log_output)
  1152. self.assertIn("refs/original", log_output)
  1153. # Run with force - should succeed
  1154. result, _stdout, _stderr = self._run_cli(
  1155. "filter-branch", "--force", "--msg-filter", "sed 's/^/[TEST3] /'"
  1156. )
  1157. self.assertEqual(result, 0)
  1158. @skipIf(sys.platform == "win32", "sed command not available on Windows")
  1159. def test_filter_branch_specific_branch(self):
  1160. """Test filter-branch on a specific branch."""
  1161. # Switch to test-branch and add a commit
  1162. self._run_cli("checkout", "test-branch")
  1163. with open(os.path.join(self.repo_path, "branch-file.txt"), "w") as f:
  1164. f.write("Branch specific file")
  1165. self._run_cli("add", "branch-file.txt")
  1166. self._run_cli("commit", "--message=Branch commit")
  1167. # Run filter-branch on the test-branch
  1168. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1169. result, stdout, _stderr = self._run_cli(
  1170. "filter-branch", "--msg-filter", "sed 's/^/[BRANCH] /'", "test-branch"
  1171. )
  1172. self.assertEqual(result, 0)
  1173. log_output = "\n".join(cm.output)
  1174. self.assertIn("Ref 'refs/heads/test-branch' was rewritten", log_output)
  1175. # Check that only test-branch was modified
  1176. result, stdout, _stderr = self._run_cli("log")
  1177. self.assertIn("[BRANCH] Branch commit", stdout)
  1178. # Switch to master and check it wasn't modified
  1179. self._run_cli("checkout", "master")
  1180. result, stdout, _stderr = self._run_cli("log")
  1181. self.assertNotIn("[BRANCH]", stdout)
  1182. def test_filter_branch_tree_filter(self):
  1183. """Test filter-branch with tree filter."""
  1184. # Use a tree filter to remove a specific file
  1185. tree_filter = "rm -f root.txt"
  1186. result, stdout, _stderr = self._run_cli(
  1187. "filter-branch", "--tree-filter", tree_filter
  1188. )
  1189. self.assertEqual(result, 0)
  1190. # Check that the file was removed from the latest commit
  1191. # We need to check the commit tree, not the working directory
  1192. result, stdout, _stderr = self._run_cli("ls-tree", "HEAD")
  1193. self.assertNotIn("root.txt", stdout)
  1194. def test_filter_branch_index_filter(self):
  1195. """Test filter-branch with index filter."""
  1196. # Use an index filter to remove a file from the index
  1197. index_filter = "git rm --cached --ignore-unmatch root.txt"
  1198. result, _stdout, _stderr = self._run_cli(
  1199. "filter-branch", "--index-filter", index_filter
  1200. )
  1201. self.assertEqual(result, 0)
  1202. def test_filter_branch_parent_filter(self):
  1203. """Test filter-branch with parent filter."""
  1204. # Create a merge commit first
  1205. self._run_cli("checkout", "HEAD", "-b", "feature")
  1206. with open(os.path.join(self.repo_path, "feature.txt"), "w") as f:
  1207. f.write("Feature")
  1208. self._run_cli("add", "feature.txt")
  1209. self._run_cli("commit", "--message=Feature commit")
  1210. self._run_cli("checkout", "master")
  1211. self._run_cli("merge", "feature", "--message=Merge feature")
  1212. # Use parent filter to linearize history (remove second parent)
  1213. parent_filter = "cut -d' ' -f1"
  1214. result, _stdout, _stderr = self._run_cli(
  1215. "filter-branch", "--parent-filter", parent_filter
  1216. )
  1217. self.assertEqual(result, 0)
  1218. def test_filter_branch_commit_filter(self):
  1219. """Test filter-branch with commit filter."""
  1220. # Use commit filter to skip commits with certain messages
  1221. commit_filter = """
  1222. if grep -q "Modify other" <<< "$GIT_COMMIT_MESSAGE"; then
  1223. skip_commit "$@"
  1224. else
  1225. git commit-tree "$@"
  1226. fi
  1227. """
  1228. _result, _stdout, _stderr = self._run_cli(
  1229. "filter-branch", "--commit-filter", commit_filter
  1230. )
  1231. # Note: This test may fail because the commit filter syntax is simplified
  1232. # In real Git, skip_commit is a function, but our implementation may differ
  1233. def test_filter_branch_tag_name_filter(self):
  1234. """Test filter-branch with tag name filter."""
  1235. # Run filter-branch with tag name filter to rename tags
  1236. result, _stdout, _stderr = self._run_cli(
  1237. "filter-branch",
  1238. "--tag-name-filter",
  1239. "sed 's/^v/version-/'",
  1240. "--msg-filter",
  1241. "cat",
  1242. )
  1243. self.assertEqual(result, 0)
  1244. # Check that tag was renamed
  1245. self.assertIn(b"refs/tags/version-1.0", self.repo.refs.keys())
  1246. def test_filter_branch_errors(self):
  1247. """Test filter-branch error handling."""
  1248. # Test with invalid subdirectory
  1249. result, _stdout, _stderr = self._run_cli(
  1250. "filter-branch", "--subdirectory-filter", "nonexistent"
  1251. )
  1252. # Should still succeed but produce empty history
  1253. self.assertEqual(result, 0)
  1254. def test_filter_branch_no_args(self):
  1255. """Test filter-branch with no arguments."""
  1256. # Should work as no-op
  1257. result, _stdout, _stderr = self._run_cli("filter-branch")
  1258. self.assertEqual(result, 0)
  1259. class ShowCommandTest(DulwichCliTestCase):
  1260. """Tests for show command."""
  1261. def test_show_commit(self):
  1262. # Create a commit
  1263. test_file = os.path.join(self.repo_path, "test.txt")
  1264. with open(test_file, "w") as f:
  1265. f.write("test content")
  1266. self._run_cli("add", "test.txt")
  1267. self._run_cli("commit", "--message=Test commit")
  1268. _result, stdout, _stderr = self._run_cli("show", "HEAD")
  1269. self.assertIn("Test commit", stdout)
  1270. class ShowRefCommandTest(DulwichCliTestCase):
  1271. """Tests for show-ref command."""
  1272. def test_show_ref_basic(self):
  1273. """Test basic show-ref functionality."""
  1274. # Create a commit to have a HEAD ref
  1275. test_file = os.path.join(self.repo_path, "test.txt")
  1276. with open(test_file, "w") as f:
  1277. f.write("test content")
  1278. self._run_cli("add", "test.txt")
  1279. self._run_cli("commit", "--message=Test commit")
  1280. # Create a branch
  1281. self._run_cli("branch", "test-branch")
  1282. # Get the exact SHAs
  1283. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1284. test_branch_sha = self.repo.refs[b"refs/heads/test-branch"].decode()
  1285. # Run show-ref
  1286. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1287. _result, _stdout, _stderr = self._run_cli("show-ref")
  1288. output = "\n".join([record.message for record in cm.records])
  1289. expected = (
  1290. f"{master_sha} refs/heads/master\n{test_branch_sha} refs/heads/test-branch"
  1291. )
  1292. self.assertEqual(output, expected)
  1293. def test_show_ref_with_head(self):
  1294. """Test show-ref with --head option."""
  1295. # Create a commit to have a HEAD ref
  1296. test_file = os.path.join(self.repo_path, "test.txt")
  1297. with open(test_file, "w") as f:
  1298. f.write("test content")
  1299. self._run_cli("add", "test.txt")
  1300. self._run_cli("commit", "--message=Test commit")
  1301. # Get the exact SHAs
  1302. head_sha = self.repo.refs[b"HEAD"].decode()
  1303. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1304. # Run show-ref with --head
  1305. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1306. _result, _stdout, _stderr = self._run_cli("show-ref", "--head")
  1307. output = "\n".join([record.message for record in cm.records])
  1308. expected = f"{head_sha} HEAD\n{master_sha} refs/heads/master"
  1309. self.assertEqual(output, expected)
  1310. def test_show_ref_with_pattern(self):
  1311. """Test show-ref with pattern matching."""
  1312. # Create commits and branches
  1313. test_file = os.path.join(self.repo_path, "test.txt")
  1314. with open(test_file, "w") as f:
  1315. f.write("test content")
  1316. self._run_cli("add", "test.txt")
  1317. self._run_cli("commit", "--message=Test commit")
  1318. self._run_cli("branch", "feature-1")
  1319. self._run_cli("branch", "feature-2")
  1320. self._run_cli("branch", "bugfix-1")
  1321. # Get the exact SHA for master
  1322. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1323. # Test pattern matching for "master"
  1324. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1325. _result, _stdout, _stderr = self._run_cli("show-ref", "master")
  1326. output = "\n".join([record.message for record in cm.records])
  1327. expected = f"{master_sha} refs/heads/master"
  1328. self.assertEqual(output, expected)
  1329. def test_show_ref_branches_only(self):
  1330. """Test show-ref with --branches option."""
  1331. # Create commits and a tag
  1332. test_file = os.path.join(self.repo_path, "test.txt")
  1333. with open(test_file, "w") as f:
  1334. f.write("test content")
  1335. self._run_cli("add", "test.txt")
  1336. self._run_cli("commit", "--message=Test commit")
  1337. self._run_cli("tag", "v1.0")
  1338. # Get the exact SHA for master
  1339. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1340. # Run show-ref with --branches
  1341. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1342. _result, _stdout, _stderr = self._run_cli("show-ref", "--branches")
  1343. output = "\n".join([record.message for record in cm.records])
  1344. expected = f"{master_sha} refs/heads/master"
  1345. self.assertEqual(output, expected)
  1346. def test_show_ref_tags_only(self):
  1347. """Test show-ref with --tags option."""
  1348. # Create commits and tags
  1349. test_file = os.path.join(self.repo_path, "test.txt")
  1350. with open(test_file, "w") as f:
  1351. f.write("test content")
  1352. self._run_cli("add", "test.txt")
  1353. self._run_cli("commit", "--message=Test commit")
  1354. self._run_cli("tag", "v1.0")
  1355. self._run_cli("tag", "v2.0")
  1356. # Get the exact SHAs for tags
  1357. v1_sha = self.repo.refs[b"refs/tags/v1.0"].decode()
  1358. v2_sha = self.repo.refs[b"refs/tags/v2.0"].decode()
  1359. # Run show-ref with --tags
  1360. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1361. _result, _stdout, _stderr = self._run_cli("show-ref", "--tags")
  1362. output = "\n".join([record.message for record in cm.records])
  1363. expected = f"{v1_sha} refs/tags/v1.0\n{v2_sha} refs/tags/v2.0"
  1364. self.assertEqual(output, expected)
  1365. def test_show_ref_hash_only(self):
  1366. """Test show-ref with --hash option to show only OID."""
  1367. # Create a commit
  1368. test_file = os.path.join(self.repo_path, "test.txt")
  1369. with open(test_file, "w") as f:
  1370. f.write("test content")
  1371. self._run_cli("add", "test.txt")
  1372. self._run_cli("commit", "--message=Test commit")
  1373. # Get the exact SHA for master
  1374. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1375. # Run show-ref with --hash
  1376. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1377. _result, _stdout, _stderr = self._run_cli(
  1378. "show-ref", "--hash", "--", "master"
  1379. )
  1380. output = "\n".join([record.message for record in cm.records])
  1381. expected = f"{master_sha}"
  1382. self.assertEqual(output, expected)
  1383. def test_show_ref_verify(self):
  1384. """Test show-ref with --verify option for exact matching."""
  1385. # Create a commit
  1386. test_file = os.path.join(self.repo_path, "test.txt")
  1387. with open(test_file, "w") as f:
  1388. f.write("test content")
  1389. self._run_cli("add", "test.txt")
  1390. self._run_cli("commit", "--message=Test commit")
  1391. # Get the exact SHA for master
  1392. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1393. # Verify with exact ref path should succeed
  1394. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1395. result, _stdout, _stderr = self._run_cli(
  1396. "show-ref", "--verify", "refs/heads/master"
  1397. )
  1398. self.assertEqual(result, 0)
  1399. output = "\n".join([record.message for record in cm.records])
  1400. expected = f"{master_sha} refs/heads/master"
  1401. self.assertEqual(output, expected)
  1402. # Verify with partial name should fail
  1403. result, _stdout, _stderr = self._run_cli("show-ref", "--verify", "master")
  1404. self.assertEqual(result, 1)
  1405. def test_show_ref_exists(self):
  1406. """Test show-ref with --exists option."""
  1407. # Create a commit
  1408. test_file = os.path.join(self.repo_path, "test.txt")
  1409. with open(test_file, "w") as f:
  1410. f.write("test content")
  1411. self._run_cli("add", "test.txt")
  1412. self._run_cli("commit", "--message=Test commit")
  1413. # Check if existing ref exists
  1414. result, _stdout, _stderr = self._run_cli(
  1415. "show-ref", "--exists", "refs/heads/master"
  1416. )
  1417. self.assertEqual(result, 0)
  1418. # Check if non-existing ref exists
  1419. result, _stdout, _stderr = self._run_cli(
  1420. "show-ref", "--exists", "refs/heads/nonexistent"
  1421. )
  1422. self.assertEqual(result, 2)
  1423. def test_show_ref_quiet(self):
  1424. """Test show-ref with --quiet option."""
  1425. # Create a commit
  1426. test_file = os.path.join(self.repo_path, "test.txt")
  1427. with open(test_file, "w") as f:
  1428. f.write("test content")
  1429. self._run_cli("add", "test.txt")
  1430. self._run_cli("commit", "--message=Test commit")
  1431. # Run show-ref with --quiet - should not log anything
  1432. result, _stdout, _stderr = self._run_cli("show-ref", "--quiet")
  1433. self.assertEqual(result, 0)
  1434. def test_show_ref_abbrev(self):
  1435. """Test show-ref with --abbrev option."""
  1436. # Create a commit
  1437. test_file = os.path.join(self.repo_path, "test.txt")
  1438. with open(test_file, "w") as f:
  1439. f.write("test content")
  1440. self._run_cli("add", "test.txt")
  1441. self._run_cli("commit", "--message=Test commit")
  1442. # Get the exact SHA for master
  1443. master_sha = self.repo.refs[b"refs/heads/master"].decode()
  1444. # Run show-ref with --abbrev=7
  1445. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1446. _result, _stdout, _stderr = self._run_cli("show-ref", "--abbrev=7")
  1447. output = "\n".join([record.message for record in cm.records])
  1448. expected = f"{master_sha[:7]} refs/heads/master"
  1449. self.assertEqual(output, expected)
  1450. def test_show_ref_no_matches(self):
  1451. """Test show-ref returns error when no matches found."""
  1452. # Create a commit
  1453. test_file = os.path.join(self.repo_path, "test.txt")
  1454. with open(test_file, "w") as f:
  1455. f.write("test content")
  1456. self._run_cli("add", "test.txt")
  1457. self._run_cli("commit", "--message=Test commit")
  1458. # Search for non-existent pattern
  1459. result, _stdout, _stderr = self._run_cli("show-ref", "nonexistent")
  1460. self.assertEqual(result, 1)
  1461. class ShowBranchCommandTest(DulwichCliTestCase):
  1462. """Tests for show-branch command."""
  1463. def test_show_branch_basic(self):
  1464. """Test basic show-branch functionality."""
  1465. # Create initial commit
  1466. test_file = os.path.join(self.repo_path, "test.txt")
  1467. with open(test_file, "w") as f:
  1468. f.write("initial content")
  1469. self._run_cli("add", "test.txt")
  1470. self._run_cli("commit", "--message=Initial commit")
  1471. # Create a branch and add a commit
  1472. self._run_cli("branch", "branch1")
  1473. self._run_cli("checkout", "branch1")
  1474. with open(test_file, "a") as f:
  1475. f.write("\nbranch1 content")
  1476. self._run_cli("add", "test.txt")
  1477. self._run_cli("commit", "--message=Branch1 commit")
  1478. # Switch back to master
  1479. self._run_cli("checkout", "master")
  1480. # Run show-branch
  1481. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1482. _result, _stdout, _stderr = self._run_cli(
  1483. "show-branch", "master", "branch1"
  1484. )
  1485. output = "\n".join([record.message for record in cm.records])
  1486. # Check exact output
  1487. expected = (
  1488. "! [branch1] Branch1 commit\n"
  1489. " ![master] Initial commit\n"
  1490. "----\n"
  1491. "* [Branch1 commit]\n"
  1492. "+* [Initial commit]"
  1493. )
  1494. self.assertEqual(expected, output)
  1495. def test_show_branch_list(self):
  1496. """Test show-branch with --list option."""
  1497. # Create initial commit
  1498. test_file = os.path.join(self.repo_path, "test.txt")
  1499. with open(test_file, "w") as f:
  1500. f.write("initial content")
  1501. self._run_cli("add", "test.txt")
  1502. self._run_cli("commit", "--message=Initial commit")
  1503. # Create branches
  1504. self._run_cli("branch", "branch1")
  1505. self._run_cli("branch", "branch2")
  1506. # Run show-branch --list
  1507. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1508. _result, _stdout, _stderr = self._run_cli("show-branch", "--list")
  1509. output = "\n".join([record.message for record in cm.records])
  1510. # Check exact output (only branch headers, no separator)
  1511. expected = (
  1512. "! [branch1] Initial commit\n"
  1513. " ! [branch2] Initial commit\n"
  1514. " ![master] Initial commit"
  1515. )
  1516. self.assertEqual(expected, output)
  1517. def test_show_branch_independent(self):
  1518. """Test show-branch with --independent option."""
  1519. # Create initial commit
  1520. test_file = os.path.join(self.repo_path, "test.txt")
  1521. with open(test_file, "w") as f:
  1522. f.write("initial content")
  1523. self._run_cli("add", "test.txt")
  1524. self._run_cli("commit", "--message=Initial commit")
  1525. # Create a branch and add a commit
  1526. self._run_cli("branch", "branch1")
  1527. self._run_cli("checkout", "branch1")
  1528. with open(test_file, "a") as f:
  1529. f.write("\nbranch1 content")
  1530. self._run_cli("add", "test.txt")
  1531. self._run_cli("commit", "--message=Branch1 commit")
  1532. # Run show-branch --independent
  1533. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1534. _result, _stdout, _stderr = self._run_cli(
  1535. "show-branch", "--independent", "master", "branch1"
  1536. )
  1537. output = "\n".join([record.message for record in cm.records])
  1538. # Only branch1 should be shown (it's not reachable from master)
  1539. expected = "branch1"
  1540. self.assertEqual(expected, output)
  1541. def test_show_branch_merge_base(self):
  1542. """Test show-branch with --merge-base option."""
  1543. # Create initial commit
  1544. test_file = os.path.join(self.repo_path, "test.txt")
  1545. with open(test_file, "w") as f:
  1546. f.write("initial content")
  1547. self._run_cli("add", "test.txt")
  1548. self._run_cli("commit", "--message=Initial commit")
  1549. # Get the initial commit SHA
  1550. initial_sha = self.repo.refs[b"HEAD"]
  1551. # Create a branch and add a commit
  1552. self._run_cli("branch", "branch1")
  1553. self._run_cli("checkout", "branch1")
  1554. with open(test_file, "a") as f:
  1555. f.write("\nbranch1 content")
  1556. self._run_cli("add", "test.txt")
  1557. self._run_cli("commit", "--message=Branch1 commit")
  1558. # Switch back to master and add a different commit
  1559. self._run_cli("checkout", "master")
  1560. with open(test_file, "a") as f:
  1561. f.write("\nmaster content")
  1562. self._run_cli("add", "test.txt")
  1563. self._run_cli("commit", "--message=Master commit")
  1564. # Run show-branch --merge-base
  1565. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1566. _result, _stdout, _stderr = self._run_cli(
  1567. "show-branch", "--merge-base", "master", "branch1"
  1568. )
  1569. output = "\n".join([record.message for record in cm.records])
  1570. # The merge base should be the initial commit SHA
  1571. expected = initial_sha.decode("ascii")
  1572. self.assertEqual(expected, output)
  1573. class FormatPatchCommandTest(DulwichCliTestCase):
  1574. """Tests for format-patch command."""
  1575. def test_format_patch_single_commit(self):
  1576. # Create a commit with actual content
  1577. from dulwich.objects import Blob, Tree
  1578. # Initial commit
  1579. tree1 = Tree()
  1580. self.repo.object_store.add_object(tree1)
  1581. self.repo.get_worktree().commit(
  1582. message=b"Initial commit",
  1583. tree=tree1.id,
  1584. )
  1585. # Second commit with a file
  1586. blob = Blob.from_string(b"Hello, World!\n")
  1587. self.repo.object_store.add_object(blob)
  1588. tree2 = Tree()
  1589. tree2.add(b"hello.txt", 0o100644, blob.id)
  1590. self.repo.object_store.add_object(tree2)
  1591. self.repo.get_worktree().commit(
  1592. message=b"Add hello.txt",
  1593. tree=tree2.id,
  1594. )
  1595. # Test format-patch for last commit
  1596. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1597. result, _stdout, _stderr = self._run_cli("format-patch", "-n", "1")
  1598. self.assertEqual(result, None)
  1599. log_output = "\n".join(cm.output)
  1600. self.assertIn("0001-Add-hello.txt.patch", log_output)
  1601. # Check patch contents
  1602. patch_file = os.path.join(self.repo_path, "0001-Add-hello.txt.patch")
  1603. with open(patch_file, "rb") as f:
  1604. content = f.read()
  1605. # Check header
  1606. self.assertIn(b"Subject: [PATCH 1/1] Add hello.txt", content)
  1607. self.assertIn(b"From:", content)
  1608. self.assertIn(b"Date:", content)
  1609. # Check diff content
  1610. self.assertIn(b"diff --git a/hello.txt b/hello.txt", content)
  1611. self.assertIn(b"new file mode", content)
  1612. self.assertIn(b"+Hello, World!", content)
  1613. # Check footer
  1614. self.assertIn(b"-- \nDulwich", content)
  1615. # Clean up
  1616. os.remove(patch_file)
  1617. def test_format_patch_multiple_commits(self):
  1618. from dulwich.objects import Blob, Tree
  1619. # Initial commit
  1620. tree1 = Tree()
  1621. self.repo.object_store.add_object(tree1)
  1622. self.repo.get_worktree().commit(
  1623. message=b"Initial commit",
  1624. tree=tree1.id,
  1625. )
  1626. # Second commit
  1627. blob1 = Blob.from_string(b"File 1 content\n")
  1628. self.repo.object_store.add_object(blob1)
  1629. tree2 = Tree()
  1630. tree2.add(b"file1.txt", 0o100644, blob1.id)
  1631. self.repo.object_store.add_object(tree2)
  1632. self.repo.get_worktree().commit(
  1633. message=b"Add file1.txt",
  1634. tree=tree2.id,
  1635. )
  1636. # Third commit
  1637. blob2 = Blob.from_string(b"File 2 content\n")
  1638. self.repo.object_store.add_object(blob2)
  1639. tree3 = Tree()
  1640. tree3.add(b"file1.txt", 0o100644, blob1.id)
  1641. tree3.add(b"file2.txt", 0o100644, blob2.id)
  1642. self.repo.object_store.add_object(tree3)
  1643. self.repo.get_worktree().commit(
  1644. message=b"Add file2.txt",
  1645. tree=tree3.id,
  1646. )
  1647. # Test format-patch for last 2 commits
  1648. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1649. result, _stdout, _stderr = self._run_cli("format-patch", "-n", "2")
  1650. self.assertEqual(result, None)
  1651. log_output = "\n".join(cm.output)
  1652. self.assertIn("0001-Add-file1.txt.patch", log_output)
  1653. self.assertIn("0002-Add-file2.txt.patch", log_output)
  1654. # Check first patch
  1655. with open(os.path.join(self.repo_path, "0001-Add-file1.txt.patch"), "rb") as f:
  1656. content = f.read()
  1657. self.assertIn(b"Subject: [PATCH 1/2] Add file1.txt", content)
  1658. self.assertIn(b"+File 1 content", content)
  1659. # Check second patch
  1660. with open(os.path.join(self.repo_path, "0002-Add-file2.txt.patch"), "rb") as f:
  1661. content = f.read()
  1662. self.assertIn(b"Subject: [PATCH 2/2] Add file2.txt", content)
  1663. self.assertIn(b"+File 2 content", content)
  1664. # Clean up
  1665. os.remove(os.path.join(self.repo_path, "0001-Add-file1.txt.patch"))
  1666. os.remove(os.path.join(self.repo_path, "0002-Add-file2.txt.patch"))
  1667. def test_format_patch_output_directory(self):
  1668. from dulwich.objects import Blob, Tree
  1669. # Create a commit
  1670. blob = Blob.from_string(b"Test content\n")
  1671. self.repo.object_store.add_object(blob)
  1672. tree = Tree()
  1673. tree.add(b"test.txt", 0o100644, blob.id)
  1674. self.repo.object_store.add_object(tree)
  1675. self.repo.get_worktree().commit(
  1676. message=b"Test commit",
  1677. tree=tree.id,
  1678. )
  1679. # Create output directory
  1680. output_dir = os.path.join(self.test_dir, "patches")
  1681. os.makedirs(output_dir)
  1682. # Test format-patch with output directory
  1683. result, _stdout, _stderr = self._run_cli(
  1684. "format-patch", "-o", output_dir, "-n", "1"
  1685. )
  1686. self.assertEqual(result, None)
  1687. # Check that file was created in output directory with correct content
  1688. patch_file = os.path.join(output_dir, "0001-Test-commit.patch")
  1689. self.assertTrue(os.path.exists(patch_file))
  1690. with open(patch_file, "rb") as f:
  1691. content = f.read()
  1692. self.assertIn(b"Subject: [PATCH 1/1] Test commit", content)
  1693. self.assertIn(b"+Test content", content)
  1694. def test_format_patch_commit_range(self):
  1695. from dulwich.objects import Blob, Tree
  1696. # Create commits with actual file changes
  1697. commits = []
  1698. trees = []
  1699. # Initial empty commit
  1700. tree0 = Tree()
  1701. self.repo.object_store.add_object(tree0)
  1702. trees.append(tree0)
  1703. c0 = self.repo.get_worktree().commit(
  1704. message=b"Initial commit",
  1705. tree=tree0.id,
  1706. )
  1707. commits.append(c0)
  1708. # Add three files in separate commits
  1709. for i in range(1, 4):
  1710. blob = Blob.from_string(f"Content {i}\n".encode())
  1711. self.repo.object_store.add_object(blob)
  1712. tree = Tree()
  1713. # Copy previous files
  1714. for j in range(1, i):
  1715. prev_blob_id = trees[j][f"file{j}.txt".encode()][1]
  1716. tree.add(f"file{j}.txt".encode(), 0o100644, prev_blob_id)
  1717. # Add new file
  1718. tree.add(f"file{i}.txt".encode(), 0o100644, blob.id)
  1719. self.repo.object_store.add_object(tree)
  1720. trees.append(tree)
  1721. c = self.repo.get_worktree().commit(
  1722. message=f"Add file{i}.txt".encode(),
  1723. tree=tree.id,
  1724. )
  1725. commits.append(c)
  1726. # Test format-patch with commit range (should get commits 2 and 3)
  1727. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1728. result, _stdout, _stderr = self._run_cli(
  1729. "format-patch", f"{commits[1].decode()}..{commits[3].decode()}"
  1730. )
  1731. self.assertEqual(result, None)
  1732. # Should create patches for commits 2 and 3
  1733. log_output = "\n".join(cm.output)
  1734. self.assertIn("0001-Add-file2.txt.patch", log_output)
  1735. self.assertIn("0002-Add-file3.txt.patch", log_output)
  1736. # Verify patch contents
  1737. with open(os.path.join(self.repo_path, "0001-Add-file2.txt.patch"), "rb") as f:
  1738. content = f.read()
  1739. self.assertIn(b"Subject: [PATCH 1/2] Add file2.txt", content)
  1740. self.assertIn(b"+Content 2", content)
  1741. self.assertNotIn(b"file3.txt", content) # Should not include file3
  1742. with open(os.path.join(self.repo_path, "0002-Add-file3.txt.patch"), "rb") as f:
  1743. content = f.read()
  1744. self.assertIn(b"Subject: [PATCH 2/2] Add file3.txt", content)
  1745. self.assertIn(b"+Content 3", content)
  1746. self.assertNotIn(b"file2.txt", content) # Should not modify file2
  1747. # Clean up
  1748. os.remove(os.path.join(self.repo_path, "0001-Add-file2.txt.patch"))
  1749. os.remove(os.path.join(self.repo_path, "0002-Add-file3.txt.patch"))
  1750. def test_format_patch_stdout(self):
  1751. from dulwich.objects import Blob, Tree
  1752. # Create a commit with modified file
  1753. tree1 = Tree()
  1754. blob1 = Blob.from_string(b"Original content\n")
  1755. self.repo.object_store.add_object(blob1)
  1756. tree1.add(b"file.txt", 0o100644, blob1.id)
  1757. self.repo.object_store.add_object(tree1)
  1758. self.repo.get_worktree().commit(
  1759. message=b"Initial commit",
  1760. tree=tree1.id,
  1761. )
  1762. tree2 = Tree()
  1763. blob2 = Blob.from_string(b"Modified content\n")
  1764. self.repo.object_store.add_object(blob2)
  1765. tree2.add(b"file.txt", 0o100644, blob2.id)
  1766. self.repo.object_store.add_object(tree2)
  1767. self.repo.get_worktree().commit(
  1768. message=b"Modify file.txt",
  1769. tree=tree2.id,
  1770. )
  1771. # Mock stdout as a BytesIO for binary output
  1772. stdout_stream = io.BytesIO()
  1773. stdout_stream.buffer = stdout_stream
  1774. # Run command with --stdout
  1775. old_stdout = sys.stdout
  1776. old_stderr = sys.stderr
  1777. old_cwd = os.getcwd()
  1778. try:
  1779. sys.stdout = stdout_stream
  1780. sys.stderr = io.StringIO()
  1781. os.chdir(self.repo_path)
  1782. cli.main(["format-patch", "--stdout", "-n", "1"])
  1783. finally:
  1784. sys.stdout = old_stdout
  1785. sys.stderr = old_stderr
  1786. os.chdir(old_cwd)
  1787. # Check output
  1788. stdout_stream.seek(0)
  1789. output = stdout_stream.read()
  1790. self.assertIn(b"Subject: [PATCH 1/1] Modify file.txt", output)
  1791. self.assertIn(b"diff --git a/file.txt b/file.txt", output)
  1792. self.assertIn(b"-Original content", output)
  1793. self.assertIn(b"+Modified content", output)
  1794. self.assertIn(b"-- \nDulwich", output)
  1795. def test_format_patch_empty_repo(self):
  1796. # Test with empty repository
  1797. result, stdout, _stderr = self._run_cli("format-patch", "-n", "5")
  1798. self.assertEqual(result, None)
  1799. # Should produce no output for empty repo
  1800. self.assertEqual(stdout.strip(), "")
  1801. class FetchPackCommandTest(DulwichCliTestCase):
  1802. """Tests for fetch-pack command."""
  1803. @patch("dulwich.cli.get_transport_and_path")
  1804. def test_fetch_pack_basic(self, mock_transport):
  1805. # Mock the transport
  1806. mock_client = MagicMock()
  1807. mock_transport.return_value = (mock_client, "/path/to/repo")
  1808. mock_client.fetch.return_value = None
  1809. _result, _stdout, _stderr = self._run_cli(
  1810. "fetch-pack", "git://example.com/repo.git"
  1811. )
  1812. mock_client.fetch.assert_called_once()
  1813. class LsRemoteCommandTest(DulwichCliTestCase):
  1814. """Tests for ls-remote command."""
  1815. def test_ls_remote_basic(self):
  1816. # Create a commit
  1817. test_file = os.path.join(self.repo_path, "test.txt")
  1818. with open(test_file, "w") as f:
  1819. f.write("test")
  1820. self._run_cli("add", "test.txt")
  1821. self._run_cli("commit", "--message=Initial")
  1822. # Test basic ls-remote
  1823. _result, stdout, _stderr = self._run_cli("ls-remote", self.repo_path)
  1824. lines = stdout.strip().split("\n")
  1825. self.assertTrue(any("HEAD" in line for line in lines))
  1826. self.assertTrue(any("refs/heads/master" in line for line in lines))
  1827. def test_ls_remote_symref(self):
  1828. # Create a commit
  1829. test_file = os.path.join(self.repo_path, "test.txt")
  1830. with open(test_file, "w") as f:
  1831. f.write("test")
  1832. self._run_cli("add", "test.txt")
  1833. self._run_cli("commit", "--message=Initial")
  1834. # Test ls-remote with --symref option
  1835. _result, stdout, _stderr = self._run_cli(
  1836. "ls-remote", "--symref", self.repo_path
  1837. )
  1838. lines = stdout.strip().split("\n")
  1839. # Should show symref for HEAD in exact format: "ref: refs/heads/master\tHEAD"
  1840. expected_line = "ref: refs/heads/master\tHEAD"
  1841. self.assertIn(
  1842. expected_line,
  1843. lines,
  1844. f"Expected line '{expected_line}' not found in output: {lines}",
  1845. )
  1846. class PullCommandTest(DulwichCliTestCase):
  1847. """Tests for pull command."""
  1848. @patch("dulwich.porcelain.pull")
  1849. def test_pull_basic(self, mock_pull):
  1850. _result, _stdout, _stderr = self._run_cli("pull", "origin")
  1851. mock_pull.assert_called_once()
  1852. @patch("dulwich.porcelain.pull")
  1853. def test_pull_with_refspec(self, mock_pull):
  1854. _result, _stdout, _stderr = self._run_cli("pull", "origin", "master")
  1855. mock_pull.assert_called_once()
  1856. class PushCommandTest(DulwichCliTestCase):
  1857. """Tests for push command."""
  1858. @patch("dulwich.porcelain.push")
  1859. def test_push_basic(self, mock_push):
  1860. _result, _stdout, _stderr = self._run_cli("push", "origin")
  1861. mock_push.assert_called_once()
  1862. @patch("dulwich.porcelain.push")
  1863. def test_push_force(self, mock_push):
  1864. _result, _stdout, _stderr = self._run_cli("push", "-f", "origin")
  1865. mock_push.assert_called_with(".", "origin", None, force=True)
  1866. class ArchiveCommandTest(DulwichCliTestCase):
  1867. """Tests for archive command."""
  1868. def test_archive_basic(self):
  1869. # Create a commit
  1870. test_file = os.path.join(self.repo_path, "test.txt")
  1871. with open(test_file, "w") as f:
  1872. f.write("test content")
  1873. self._run_cli("add", "test.txt")
  1874. self._run_cli("commit", "--message=Initial")
  1875. # Archive produces binary output, so use BytesIO
  1876. _result, stdout, _stderr = self._run_cli(
  1877. "archive", "HEAD", stdout_stream=io.BytesIO()
  1878. )
  1879. # Should complete without error and produce some binary output
  1880. self.assertIsInstance(stdout, bytes)
  1881. self.assertGreater(len(stdout), 0)
  1882. class ForEachRefCommandTest(DulwichCliTestCase):
  1883. """Tests for for-each-ref command."""
  1884. def test_for_each_ref(self):
  1885. # Create a commit
  1886. test_file = os.path.join(self.repo_path, "test.txt")
  1887. with open(test_file, "w") as f:
  1888. f.write("test")
  1889. self._run_cli("add", "test.txt")
  1890. self._run_cli("commit", "--message=Initial")
  1891. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1892. _result, _stdout, _stderr = self._run_cli("for-each-ref")
  1893. log_output = "\n".join(cm.output)
  1894. # Just check that we have some refs output and it contains refs/heads
  1895. self.assertTrue(len(cm.output) > 0, "Expected some ref output")
  1896. self.assertIn("refs/heads/", log_output)
  1897. class PackRefsCommandTest(DulwichCliTestCase):
  1898. """Tests for pack-refs command."""
  1899. def test_pack_refs(self):
  1900. # Create some refs
  1901. test_file = os.path.join(self.repo_path, "test.txt")
  1902. with open(test_file, "w") as f:
  1903. f.write("test")
  1904. self._run_cli("add", "test.txt")
  1905. self._run_cli("commit", "--message=Initial")
  1906. self._run_cli("branch", "test-branch")
  1907. _result, _stdout, _stderr = self._run_cli("pack-refs", "--all")
  1908. # Check that packed-refs file exists
  1909. self.assertTrue(
  1910. os.path.exists(os.path.join(self.repo_path, ".git", "packed-refs"))
  1911. )
  1912. class SubmoduleCommandTest(DulwichCliTestCase):
  1913. """Tests for submodule commands."""
  1914. def test_submodule_list(self):
  1915. # Create an initial commit so repo has a HEAD
  1916. test_file = os.path.join(self.repo_path, "test.txt")
  1917. with open(test_file, "w") as f:
  1918. f.write("test")
  1919. self._run_cli("add", "test.txt")
  1920. self._run_cli("commit", "--message=Initial")
  1921. _result, _stdout, _stderr = self._run_cli("submodule")
  1922. # Should not crash on repo without submodules
  1923. def test_submodule_init(self):
  1924. # Create .gitmodules file for init to work
  1925. gitmodules = os.path.join(self.repo_path, ".gitmodules")
  1926. with open(gitmodules, "w") as f:
  1927. f.write("") # Empty .gitmodules file
  1928. _result, _stdout, _stderr = self._run_cli("submodule", "init")
  1929. # Should not crash
  1930. class StashCommandTest(DulwichCliTestCase):
  1931. """Tests for stash commands."""
  1932. def test_stash_list_empty(self):
  1933. _result, _stdout, _stderr = self._run_cli("stash", "list")
  1934. # Should not crash on empty stash
  1935. def test_stash_push_pop(self):
  1936. # Create a file and modify it
  1937. test_file = os.path.join(self.repo_path, "test.txt")
  1938. with open(test_file, "w") as f:
  1939. f.write("initial")
  1940. self._run_cli("add", "test.txt")
  1941. self._run_cli("commit", "--message=Initial")
  1942. # Modify file
  1943. with open(test_file, "w") as f:
  1944. f.write("modified")
  1945. # Stash changes
  1946. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1947. _result, _stdout, _stderr = self._run_cli("stash", "push")
  1948. self.assertIn("Saved working directory", cm.output[0])
  1949. # Note: Dulwich stash doesn't currently update the working tree
  1950. # so the file remains modified after stash push
  1951. # Note: stash pop is not fully implemented in Dulwich yet
  1952. # so we only test stash push here
  1953. class MergeCommandTest(DulwichCliTestCase):
  1954. """Tests for merge command."""
  1955. def test_merge_basic(self):
  1956. # Create initial commit
  1957. test_file = os.path.join(self.repo_path, "test.txt")
  1958. with open(test_file, "w") as f:
  1959. f.write("initial")
  1960. self._run_cli("add", "test.txt")
  1961. self._run_cli("commit", "--message=Initial")
  1962. # Create and checkout new branch
  1963. self._run_cli("branch", "feature")
  1964. self._run_cli("checkout", "feature")
  1965. # Make changes in feature branch
  1966. with open(test_file, "w") as f:
  1967. f.write("feature changes")
  1968. self._run_cli("add", "test.txt")
  1969. self._run_cli("commit", "--message=Feature commit")
  1970. # Go back to main
  1971. self._run_cli("checkout", "master")
  1972. # Merge feature branch
  1973. _result, _stdout, _stderr = self._run_cli("merge", "feature")
  1974. class HelpCommandTest(DulwichCliTestCase):
  1975. """Tests for help command."""
  1976. def test_help_basic(self):
  1977. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1978. _result, _stdout, _stderr = self._run_cli("help")
  1979. log_output = "\n".join(cm.output)
  1980. self.assertIn("dulwich command line tool", log_output)
  1981. def test_help_all(self):
  1982. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  1983. _result, _stdout, _stderr = self._run_cli("help", "-a")
  1984. log_output = "\n".join(cm.output)
  1985. self.assertIn("Available commands:", log_output)
  1986. self.assertIn("add", log_output)
  1987. self.assertIn("commit", log_output)
  1988. class RemoteCommandTest(DulwichCliTestCase):
  1989. """Tests for remote commands."""
  1990. def test_remote_add(self):
  1991. _result, _stdout, _stderr = self._run_cli(
  1992. "remote", "add", "origin", "https://github.com/example/repo.git"
  1993. )
  1994. # Check remote was added to config
  1995. config = self.repo.get_config()
  1996. self.assertEqual(
  1997. config.get((b"remote", b"origin"), b"url"),
  1998. b"https://github.com/example/repo.git",
  1999. )
  2000. class CheckIgnoreCommandTest(DulwichCliTestCase):
  2001. """Tests for check-ignore command."""
  2002. def test_check_ignore(self):
  2003. # Create .gitignore
  2004. gitignore = os.path.join(self.repo_path, ".gitignore")
  2005. with open(gitignore, "w") as f:
  2006. f.write("*.log\n")
  2007. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2008. _result, _stdout, _stderr = self._run_cli(
  2009. "check-ignore", "test.log", "test.txt"
  2010. )
  2011. log_output = "\n".join(cm.output)
  2012. self.assertIn("test.log", log_output)
  2013. self.assertNotIn("test.txt", log_output)
  2014. class LsFilesCommandTest(DulwichCliTestCase):
  2015. """Tests for ls-files command."""
  2016. def test_ls_files(self):
  2017. # Add some files
  2018. for name in ["a.txt", "b.txt", "c.txt"]:
  2019. path = os.path.join(self.repo_path, name)
  2020. with open(path, "w") as f:
  2021. f.write(f"content of {name}")
  2022. self._run_cli("add", "a.txt", "b.txt", "c.txt")
  2023. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2024. _result, _stdout, _stderr = self._run_cli("ls-files")
  2025. log_output = "\n".join(cm.output)
  2026. self.assertIn("a.txt", log_output)
  2027. self.assertIn("b.txt", log_output)
  2028. self.assertIn("c.txt", log_output)
  2029. class LsTreeCommandTest(DulwichCliTestCase):
  2030. """Tests for ls-tree command."""
  2031. def test_ls_tree(self):
  2032. # Create a directory structure
  2033. os.mkdir(os.path.join(self.repo_path, "subdir"))
  2034. with open(os.path.join(self.repo_path, "file.txt"), "w") as f:
  2035. f.write("file content")
  2036. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  2037. f.write("nested content")
  2038. self._run_cli("add", ".")
  2039. self._run_cli("commit", "--message=Initial")
  2040. _result, stdout, _stderr = self._run_cli("ls-tree", "HEAD")
  2041. self.assertIn("file.txt", stdout)
  2042. self.assertIn("subdir", stdout)
  2043. def test_ls_tree_recursive(self):
  2044. # Create nested structure
  2045. os.mkdir(os.path.join(self.repo_path, "subdir"))
  2046. with open(os.path.join(self.repo_path, "subdir", "nested.txt"), "w") as f:
  2047. f.write("nested")
  2048. self._run_cli("add", ".")
  2049. self._run_cli("commit", "--message=Initial")
  2050. _result, stdout, _stderr = self._run_cli("ls-tree", "-r", "HEAD")
  2051. self.assertIn("subdir/nested.txt", stdout)
  2052. class DescribeCommandTest(DulwichCliTestCase):
  2053. """Tests for describe command."""
  2054. def test_describe(self):
  2055. # Create tagged commit
  2056. test_file = os.path.join(self.repo_path, "test.txt")
  2057. with open(test_file, "w") as f:
  2058. f.write("test")
  2059. self._run_cli("add", "test.txt")
  2060. self._run_cli("commit", "--message=Initial")
  2061. self._run_cli("tag", "v1.0")
  2062. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2063. _result, _stdout, _stderr = self._run_cli("describe")
  2064. self.assertIn("v1.0", cm.output[0])
  2065. class FsckCommandTest(DulwichCliTestCase):
  2066. """Tests for fsck command."""
  2067. def test_fsck(self):
  2068. # Create a commit
  2069. test_file = os.path.join(self.repo_path, "test.txt")
  2070. with open(test_file, "w") as f:
  2071. f.write("test")
  2072. self._run_cli("add", "test.txt")
  2073. self._run_cli("commit", "--message=Initial")
  2074. _result, _stdout, _stderr = self._run_cli("fsck")
  2075. # Should complete without errors
  2076. class GrepCommandTest(DulwichCliTestCase):
  2077. """Tests for grep command."""
  2078. def test_grep_basic(self):
  2079. # Create test files
  2080. with open(os.path.join(self.repo_path, "file1.txt"), "w") as f:
  2081. f.write("hello world\n")
  2082. with open(os.path.join(self.repo_path, "file2.txt"), "w") as f:
  2083. f.write("foo bar\n")
  2084. self._run_cli("add", "file1.txt", "file2.txt")
  2085. self._run_cli("commit", "--message=Add files")
  2086. _result, stdout, _stderr = self._run_cli("grep", "world")
  2087. self.assertEqual("file1.txt:hello world\n", stdout.replace("\r\n", "\n"))
  2088. def test_grep_line_numbers(self):
  2089. with open(os.path.join(self.repo_path, "test.txt"), "w") as f:
  2090. f.write("line1\nline2\nline3\n")
  2091. self._run_cli("add", "test.txt")
  2092. self._run_cli("commit", "--message=Add test")
  2093. _result, stdout, _stderr = self._run_cli("grep", "-n", "line")
  2094. self.assertEqual(
  2095. "test.txt:1:line1\ntest.txt:2:line2\ntest.txt:3:line3\n",
  2096. stdout.replace("\r\n", "\n"),
  2097. )
  2098. def test_grep_case_insensitive(self):
  2099. with open(os.path.join(self.repo_path, "case.txt"), "w") as f:
  2100. f.write("Hello World\n")
  2101. self._run_cli("add", "case.txt")
  2102. self._run_cli("commit", "--message=Add case")
  2103. _result, stdout, _stderr = self._run_cli("grep", "-i", "hello")
  2104. self.assertEqual("case.txt:Hello World\n", stdout.replace("\r\n", "\n"))
  2105. def test_grep_no_matches(self):
  2106. with open(os.path.join(self.repo_path, "empty.txt"), "w") as f:
  2107. f.write("nothing here\n")
  2108. self._run_cli("add", "empty.txt")
  2109. self._run_cli("commit", "--message=Add empty")
  2110. _result, stdout, _stderr = self._run_cli("grep", "nonexistent")
  2111. self.assertEqual("", stdout)
  2112. class RepackCommandTest(DulwichCliTestCase):
  2113. """Tests for repack command."""
  2114. def test_repack(self):
  2115. # Create some objects
  2116. for i in range(5):
  2117. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  2118. with open(test_file, "w") as f:
  2119. f.write(f"content {i}")
  2120. self._run_cli("add", f"test{i}.txt")
  2121. self._run_cli("commit", f"--message=Commit {i}")
  2122. _result, _stdout, _stderr = self._run_cli("repack")
  2123. # Should create pack files
  2124. pack_dir = os.path.join(self.repo_path, ".git", "objects", "pack")
  2125. self.assertTrue(any(f.endswith(".pack") for f in os.listdir(pack_dir)))
  2126. def test_repack_write_bitmap_index(self):
  2127. """Test repack with --write-bitmap-index flag."""
  2128. # Create some objects
  2129. for i in range(5):
  2130. test_file = os.path.join(self.repo_path, f"test{i}.txt")
  2131. with open(test_file, "w") as f:
  2132. f.write(f"content {i}")
  2133. self._run_cli("add", f"test{i}.txt")
  2134. self._run_cli("commit", f"--message=Commit {i}")
  2135. _result, _stdout, _stderr = self._run_cli("repack", "--write-bitmap-index")
  2136. # Should create pack and bitmap files
  2137. pack_dir = os.path.join(self.repo_path, ".git", "objects", "pack")
  2138. self.assertTrue(any(f.endswith(".pack") for f in os.listdir(pack_dir)))
  2139. self.assertTrue(any(f.endswith(".bitmap") for f in os.listdir(pack_dir)))
  2140. class ResetCommandTest(DulwichCliTestCase):
  2141. """Tests for reset command."""
  2142. def test_reset_soft(self):
  2143. # Create commits
  2144. test_file = os.path.join(self.repo_path, "test.txt")
  2145. with open(test_file, "w") as f:
  2146. f.write("first")
  2147. self._run_cli("add", "test.txt")
  2148. self._run_cli("commit", "--message=First")
  2149. first_commit = self.repo.head()
  2150. with open(test_file, "w") as f:
  2151. f.write("second")
  2152. self._run_cli("add", "test.txt")
  2153. self._run_cli("commit", "--message=Second")
  2154. # Reset soft
  2155. _result, _stdout, _stderr = self._run_cli(
  2156. "reset", "--soft", first_commit.decode()
  2157. )
  2158. # HEAD should be at first commit
  2159. self.assertEqual(self.repo.head(), first_commit)
  2160. class WriteTreeCommandTest(DulwichCliTestCase):
  2161. """Tests for write-tree command."""
  2162. def test_write_tree(self):
  2163. # Create and add files
  2164. test_file = os.path.join(self.repo_path, "test.txt")
  2165. with open(test_file, "w") as f:
  2166. f.write("test")
  2167. self._run_cli("add", "test.txt")
  2168. _result, stdout, _stderr = self._run_cli("write-tree")
  2169. # Should output tree SHA
  2170. self.assertEqual(len(stdout.strip()), 40)
  2171. class UpdateServerInfoCommandTest(DulwichCliTestCase):
  2172. """Tests for update-server-info command."""
  2173. def test_update_server_info(self):
  2174. _result, _stdout, _stderr = self._run_cli("update-server-info")
  2175. # Should create info/refs file
  2176. info_refs = os.path.join(self.repo_path, ".git", "info", "refs")
  2177. self.assertTrue(os.path.exists(info_refs))
  2178. class SymbolicRefCommandTest(DulwichCliTestCase):
  2179. """Tests for symbolic-ref command."""
  2180. def test_symbolic_ref(self):
  2181. # Create a branch
  2182. test_file = os.path.join(self.repo_path, "test.txt")
  2183. with open(test_file, "w") as f:
  2184. f.write("test")
  2185. self._run_cli("add", "test.txt")
  2186. self._run_cli("commit", "--message=Initial")
  2187. self._run_cli("branch", "test-branch")
  2188. _result, _stdout, _stderr = self._run_cli(
  2189. "symbolic-ref", "HEAD", "refs/heads/test-branch"
  2190. )
  2191. # HEAD should now point to test-branch
  2192. self.assertEqual(
  2193. self.repo.refs.read_ref(b"HEAD"), b"ref: refs/heads/test-branch"
  2194. )
  2195. class BundleCommandTest(DulwichCliTestCase):
  2196. """Tests for bundle commands."""
  2197. def setUp(self):
  2198. super().setUp()
  2199. # Create a basic repository with some commits for bundle testing
  2200. # Create initial commit
  2201. test_file = os.path.join(self.repo_path, "file1.txt")
  2202. with open(test_file, "w") as f:
  2203. f.write("Content of file1\n")
  2204. self._run_cli("add", "file1.txt")
  2205. self._run_cli("commit", "--message=Initial commit")
  2206. # Create second commit
  2207. test_file2 = os.path.join(self.repo_path, "file2.txt")
  2208. with open(test_file2, "w") as f:
  2209. f.write("Content of file2\n")
  2210. self._run_cli("add", "file2.txt")
  2211. self._run_cli("commit", "--message=Add file2")
  2212. # Create a branch and tag for testing
  2213. self._run_cli("branch", "feature")
  2214. self._run_cli("tag", "v1.0")
  2215. def test_bundle_create_basic(self):
  2216. """Test basic bundle creation."""
  2217. bundle_file = os.path.join(self.test_dir, "test.bundle")
  2218. result, _stdout, _stderr = self._run_cli(
  2219. "bundle", "create", bundle_file, "HEAD"
  2220. )
  2221. self.assertEqual(result, 0)
  2222. self.assertTrue(os.path.exists(bundle_file))
  2223. self.assertGreater(os.path.getsize(bundle_file), 0)
  2224. def test_bundle_create_all_refs(self):
  2225. """Test bundle creation with --all flag."""
  2226. bundle_file = os.path.join(self.test_dir, "all.bundle")
  2227. result, _stdout, _stderr = self._run_cli(
  2228. "bundle", "create", "--all", bundle_file
  2229. )
  2230. self.assertEqual(result, 0)
  2231. self.assertTrue(os.path.exists(bundle_file))
  2232. def test_bundle_create_specific_refs(self):
  2233. """Test bundle creation with specific refs."""
  2234. bundle_file = os.path.join(self.test_dir, "refs.bundle")
  2235. # Only use HEAD since feature branch may not exist
  2236. result, _stdout, _stderr = self._run_cli(
  2237. "bundle", "create", bundle_file, "HEAD"
  2238. )
  2239. self.assertEqual(result, 0)
  2240. self.assertTrue(os.path.exists(bundle_file))
  2241. def test_bundle_create_with_range(self):
  2242. """Test bundle creation with commit range."""
  2243. # Get the first commit SHA by looking at the log
  2244. result, stdout, _stderr = self._run_cli("log", "--reverse")
  2245. lines = stdout.strip().split("\n")
  2246. # Find first commit line that contains a SHA
  2247. first_commit = None
  2248. for line in lines:
  2249. if line.startswith("commit "):
  2250. first_commit = line.split()[1][:8] # Get short SHA
  2251. break
  2252. if first_commit:
  2253. bundle_file = os.path.join(self.test_dir, "range.bundle")
  2254. result, stdout, _stderr = self._run_cli(
  2255. "bundle", "create", bundle_file, f"{first_commit}..HEAD"
  2256. )
  2257. self.assertEqual(result, 0)
  2258. self.assertTrue(os.path.exists(bundle_file))
  2259. else:
  2260. self.skipTest("Could not determine first commit SHA")
  2261. def test_bundle_create_to_stdout(self):
  2262. """Test bundle creation to stdout."""
  2263. result, stdout, _stderr = self._run_cli("bundle", "create", "-", "HEAD")
  2264. self.assertEqual(result, 0)
  2265. self.assertGreater(len(stdout), 0)
  2266. # Bundle output is binary, so check it's not empty
  2267. self.assertIsInstance(stdout, (str, bytes))
  2268. def test_bundle_create_no_refs(self):
  2269. """Test bundle creation with no refs specified."""
  2270. bundle_file = os.path.join(self.test_dir, "noref.bundle")
  2271. with self.assertLogs("dulwich.cli", level="ERROR") as cm:
  2272. result, _stdout, _stderr = self._run_cli("bundle", "create", bundle_file)
  2273. self.assertEqual(result, 1)
  2274. self.assertIn("No refs specified", cm.output[0])
  2275. def test_bundle_create_empty_bundle_refused(self):
  2276. """Test that empty bundles are refused."""
  2277. bundle_file = os.path.join(self.test_dir, "empty.bundle")
  2278. # Try to create bundle with non-existent ref - this should fail with KeyError
  2279. with self.assertRaises(KeyError):
  2280. _result, _stdout, _stderr = self._run_cli(
  2281. "bundle", "create", bundle_file, "nonexistent-ref"
  2282. )
  2283. def test_bundle_verify_valid(self):
  2284. """Test bundle verification of valid bundle."""
  2285. bundle_file = os.path.join(self.test_dir, "valid.bundle")
  2286. # First create a bundle
  2287. result, _stdout, _stderr = self._run_cli(
  2288. "bundle", "create", bundle_file, "HEAD"
  2289. )
  2290. self.assertEqual(result, 0)
  2291. # Now verify it
  2292. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2293. result, _stdout, _stderr = self._run_cli("bundle", "verify", bundle_file)
  2294. self.assertEqual(result, 0)
  2295. self.assertIn("valid and can be applied", cm.output[0])
  2296. def test_bundle_verify_quiet(self):
  2297. """Test bundle verification with quiet flag."""
  2298. bundle_file = os.path.join(self.test_dir, "quiet.bundle")
  2299. # Create bundle
  2300. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2301. # Verify quietly
  2302. result, stdout, _stderr = self._run_cli(
  2303. "bundle", "verify", "--quiet", bundle_file
  2304. )
  2305. self.assertEqual(result, 0)
  2306. self.assertEqual(stdout.strip(), "")
  2307. def test_bundle_verify_from_stdin(self):
  2308. """Test bundle verification from stdin."""
  2309. bundle_file = os.path.join(self.test_dir, "stdin.bundle")
  2310. # Create bundle
  2311. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2312. # Read bundle content
  2313. with open(bundle_file, "rb") as f:
  2314. bundle_content = f.read()
  2315. # Mock stdin with bundle content
  2316. old_stdin = sys.stdin
  2317. try:
  2318. sys.stdin = io.BytesIO(bundle_content)
  2319. sys.stdin.buffer = sys.stdin
  2320. result, _stdout, _stderr = self._run_cli("bundle", "verify", "-")
  2321. self.assertEqual(result, 0)
  2322. finally:
  2323. sys.stdin = old_stdin
  2324. def test_bundle_list_heads(self):
  2325. """Test listing bundle heads."""
  2326. bundle_file = os.path.join(self.test_dir, "heads.bundle")
  2327. # Create bundle with HEAD only
  2328. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2329. # List heads
  2330. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2331. result, _stdout, _stderr = self._run_cli(
  2332. "bundle", "list-heads", bundle_file
  2333. )
  2334. self.assertEqual(result, 0)
  2335. # Should contain at least the HEAD reference
  2336. self.assertTrue(len(cm.output) > 0)
  2337. def test_bundle_list_heads_specific_refs(self):
  2338. """Test listing specific bundle heads."""
  2339. bundle_file = os.path.join(self.test_dir, "specific.bundle")
  2340. # Create bundle with HEAD
  2341. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2342. # List heads without filtering
  2343. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2344. result, _stdout, _stderr = self._run_cli(
  2345. "bundle", "list-heads", bundle_file
  2346. )
  2347. self.assertEqual(result, 0)
  2348. # Should contain some reference
  2349. self.assertTrue(len(cm.output) > 0)
  2350. def test_bundle_list_heads_from_stdin(self):
  2351. """Test listing bundle heads from stdin."""
  2352. bundle_file = os.path.join(self.test_dir, "stdin-heads.bundle")
  2353. # Create bundle
  2354. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2355. # Read bundle content
  2356. with open(bundle_file, "rb") as f:
  2357. bundle_content = f.read()
  2358. # Mock stdin
  2359. old_stdin = sys.stdin
  2360. try:
  2361. sys.stdin = io.BytesIO(bundle_content)
  2362. sys.stdin.buffer = sys.stdin
  2363. result, _stdout, _stderr = self._run_cli("bundle", "list-heads", "-")
  2364. self.assertEqual(result, 0)
  2365. finally:
  2366. sys.stdin = old_stdin
  2367. def test_bundle_unbundle(self):
  2368. """Test bundle unbundling."""
  2369. bundle_file = os.path.join(self.test_dir, "unbundle.bundle")
  2370. # Create bundle
  2371. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2372. # Unbundle
  2373. result, _stdout, _stderr = self._run_cli("bundle", "unbundle", bundle_file)
  2374. self.assertEqual(result, 0)
  2375. def test_bundle_unbundle_specific_refs(self):
  2376. """Test unbundling specific refs."""
  2377. bundle_file = os.path.join(self.test_dir, "unbundle-specific.bundle")
  2378. # Create bundle with HEAD
  2379. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2380. # Unbundle only HEAD
  2381. result, _stdout, _stderr = self._run_cli(
  2382. "bundle", "unbundle", bundle_file, "HEAD"
  2383. )
  2384. self.assertEqual(result, 0)
  2385. def test_bundle_unbundle_from_stdin(self):
  2386. """Test unbundling from stdin."""
  2387. bundle_file = os.path.join(self.test_dir, "stdin-unbundle.bundle")
  2388. # Create bundle
  2389. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2390. # Read bundle content to simulate stdin
  2391. with open(bundle_file, "rb") as f:
  2392. bundle_content = f.read()
  2393. # Mock stdin with bundle content
  2394. old_stdin = sys.stdin
  2395. try:
  2396. # Create a BytesIO object with buffer attribute
  2397. mock_stdin = io.BytesIO(bundle_content)
  2398. mock_stdin.buffer = mock_stdin
  2399. sys.stdin = mock_stdin
  2400. result, _stdout, _stderr = self._run_cli("bundle", "unbundle", "-")
  2401. self.assertEqual(result, 0)
  2402. finally:
  2403. sys.stdin = old_stdin
  2404. def test_bundle_unbundle_with_progress(self):
  2405. """Test unbundling with progress output."""
  2406. bundle_file = os.path.join(self.test_dir, "progress.bundle")
  2407. # Create bundle
  2408. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2409. # Unbundle with progress
  2410. result, _stdout, _stderr = self._run_cli(
  2411. "bundle", "unbundle", "--progress", bundle_file
  2412. )
  2413. self.assertEqual(result, 0)
  2414. def test_bundle_create_with_progress(self):
  2415. """Test bundle creation with progress output."""
  2416. bundle_file = os.path.join(self.test_dir, "create-progress.bundle")
  2417. result, _stdout, _stderr = self._run_cli(
  2418. "bundle", "create", "--progress", bundle_file, "HEAD"
  2419. )
  2420. self.assertEqual(result, 0)
  2421. self.assertTrue(os.path.exists(bundle_file))
  2422. def test_bundle_create_with_quiet(self):
  2423. """Test bundle creation with quiet flag."""
  2424. bundle_file = os.path.join(self.test_dir, "quiet-create.bundle")
  2425. result, _stdout, _stderr = self._run_cli(
  2426. "bundle", "create", "--quiet", bundle_file, "HEAD"
  2427. )
  2428. self.assertEqual(result, 0)
  2429. self.assertTrue(os.path.exists(bundle_file))
  2430. def test_bundle_create_version_2(self):
  2431. """Test bundle creation with specific version."""
  2432. bundle_file = os.path.join(self.test_dir, "v2.bundle")
  2433. result, _stdout, _stderr = self._run_cli(
  2434. "bundle", "create", "--version", "2", bundle_file, "HEAD"
  2435. )
  2436. self.assertEqual(result, 0)
  2437. self.assertTrue(os.path.exists(bundle_file))
  2438. def test_bundle_create_version_3(self):
  2439. """Test bundle creation with version 3."""
  2440. bundle_file = os.path.join(self.test_dir, "v3.bundle")
  2441. result, _stdout, _stderr = self._run_cli(
  2442. "bundle", "create", "--version", "3", bundle_file, "HEAD"
  2443. )
  2444. self.assertEqual(result, 0)
  2445. self.assertTrue(os.path.exists(bundle_file))
  2446. def test_bundle_invalid_subcommand(self):
  2447. """Test invalid bundle subcommand."""
  2448. with self.assertLogs("dulwich.cli", level="ERROR") as cm:
  2449. result, _stdout, _stderr = self._run_cli("bundle", "invalid-command")
  2450. self.assertEqual(result, 1)
  2451. self.assertIn("Unknown bundle subcommand", cm.output[0])
  2452. def test_bundle_no_subcommand(self):
  2453. """Test bundle command with no subcommand."""
  2454. with self.assertLogs("dulwich.cli", level="ERROR") as cm:
  2455. result, _stdout, _stderr = self._run_cli("bundle")
  2456. self.assertEqual(result, 1)
  2457. self.assertIn("Usage: bundle", cm.output[0])
  2458. def test_bundle_create_with_stdin_refs(self):
  2459. """Test bundle creation reading refs from stdin."""
  2460. bundle_file = os.path.join(self.test_dir, "stdin-refs.bundle")
  2461. # Mock stdin with refs
  2462. old_stdin = sys.stdin
  2463. try:
  2464. sys.stdin = io.StringIO("master\nfeature\n")
  2465. result, _stdout, _stderr = self._run_cli(
  2466. "bundle", "create", "--stdin", bundle_file
  2467. )
  2468. self.assertEqual(result, 0)
  2469. self.assertTrue(os.path.exists(bundle_file))
  2470. finally:
  2471. sys.stdin = old_stdin
  2472. def test_bundle_verify_missing_prerequisites(self):
  2473. """Test bundle verification with missing prerequisites."""
  2474. # Create a simple bundle first
  2475. bundle_file = os.path.join(self.test_dir, "prereq.bundle")
  2476. self._run_cli("bundle", "create", bundle_file, "HEAD")
  2477. # Create a new repo to simulate missing objects
  2478. new_repo_path = os.path.join(self.test_dir, "new_repo")
  2479. os.mkdir(new_repo_path)
  2480. new_repo = Repo.init(new_repo_path)
  2481. new_repo.close()
  2482. # Try to verify in new repo
  2483. old_cwd = os.getcwd()
  2484. try:
  2485. os.chdir(new_repo_path)
  2486. result, _stdout, _stderr = self._run_cli("bundle", "verify", bundle_file)
  2487. # Just check that verification runs - result depends on bundle content
  2488. self.assertIn(result, [0, 1])
  2489. finally:
  2490. os.chdir(old_cwd)
  2491. def test_bundle_create_with_committish_range(self):
  2492. """Test bundle creation with commit range using parse_committish_range."""
  2493. # Create additional commits for range testing
  2494. test_file3 = os.path.join(self.repo_path, "file3.txt")
  2495. with open(test_file3, "w") as f:
  2496. f.write("Content of file3\n")
  2497. self._run_cli("add", "file3.txt")
  2498. self._run_cli("commit", "--message=Add file3")
  2499. # Get commit SHAs
  2500. result, stdout, _stderr = self._run_cli("log")
  2501. lines = stdout.strip().split("\n")
  2502. # Extract SHAs from commit lines
  2503. commits = []
  2504. for line in lines:
  2505. if line.startswith("commit:"):
  2506. sha = line.split()[1]
  2507. commits.append(sha[:8]) # Get short SHA
  2508. # We should have exactly 3 commits: Add file3, Add file2, Initial commit
  2509. self.assertEqual(len(commits), 3)
  2510. bundle_file = os.path.join(self.test_dir, "range-test.bundle")
  2511. # Test with commit range using .. syntax
  2512. # Create a bundle containing commits reachable from commits[0] but not from commits[2]
  2513. result, stdout, stderr = self._run_cli(
  2514. "bundle", "create", bundle_file, f"{commits[2]}..HEAD"
  2515. )
  2516. if result != 0:
  2517. self.fail(
  2518. f"Bundle create failed with exit code {result}. stdout: {stdout!r}, stderr: {stderr!r}"
  2519. )
  2520. self.assertEqual(result, 0)
  2521. self.assertTrue(os.path.exists(bundle_file))
  2522. # Verify the bundle was created
  2523. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2524. result, stdout, stderr = self._run_cli("bundle", "verify", bundle_file)
  2525. self.assertEqual(result, 0)
  2526. self.assertIn("valid and can be applied", cm.output[0])
  2527. class FormatBytesTestCase(TestCase):
  2528. """Tests for format_bytes function."""
  2529. def test_bytes(self):
  2530. """Test formatting bytes."""
  2531. self.assertEqual("0.0 B", format_bytes(0))
  2532. self.assertEqual("1.0 B", format_bytes(1))
  2533. self.assertEqual("512.0 B", format_bytes(512))
  2534. self.assertEqual("1023.0 B", format_bytes(1023))
  2535. def test_kilobytes(self):
  2536. """Test formatting kilobytes."""
  2537. self.assertEqual("1.0 KB", format_bytes(1024))
  2538. self.assertEqual("1.5 KB", format_bytes(1536))
  2539. self.assertEqual("2.0 KB", format_bytes(2048))
  2540. self.assertEqual("1023.0 KB", format_bytes(1024 * 1023))
  2541. def test_megabytes(self):
  2542. """Test formatting megabytes."""
  2543. self.assertEqual("1.0 MB", format_bytes(1024 * 1024))
  2544. self.assertEqual("1.5 MB", format_bytes(1024 * 1024 * 1.5))
  2545. self.assertEqual("10.0 MB", format_bytes(1024 * 1024 * 10))
  2546. self.assertEqual("1023.0 MB", format_bytes(1024 * 1024 * 1023))
  2547. def test_gigabytes(self):
  2548. """Test formatting gigabytes."""
  2549. self.assertEqual("1.0 GB", format_bytes(1024 * 1024 * 1024))
  2550. self.assertEqual("2.5 GB", format_bytes(1024 * 1024 * 1024 * 2.5))
  2551. self.assertEqual("1023.0 GB", format_bytes(1024 * 1024 * 1024 * 1023))
  2552. def test_terabytes(self):
  2553. """Test formatting terabytes."""
  2554. self.assertEqual("1.0 TB", format_bytes(1024 * 1024 * 1024 * 1024))
  2555. self.assertEqual("5.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 5))
  2556. self.assertEqual("1000.0 TB", format_bytes(1024 * 1024 * 1024 * 1024 * 1000))
  2557. class GetPagerTest(TestCase):
  2558. """Tests for get_pager function."""
  2559. def setUp(self):
  2560. super().setUp()
  2561. # Save original environment
  2562. self.original_env = os.environ.copy()
  2563. # Clear pager-related environment variables
  2564. for var in ["DULWICH_PAGER", "GIT_PAGER", "PAGER"]:
  2565. os.environ.pop(var, None)
  2566. # Reset the global pager disable flag
  2567. cli.get_pager._disabled = False
  2568. def tearDown(self):
  2569. super().tearDown()
  2570. # Restore original environment
  2571. os.environ.clear()
  2572. os.environ.update(self.original_env)
  2573. # Reset the global pager disable flag
  2574. cli.get_pager._disabled = False
  2575. def test_pager_disabled_globally(self):
  2576. """Test that globally disabled pager returns stdout wrapper."""
  2577. cli.disable_pager()
  2578. pager = cli.get_pager()
  2579. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2580. self.assertEqual(pager.stream, sys.stdout)
  2581. def test_pager_not_tty(self):
  2582. """Test that pager is disabled when stdout is not a TTY."""
  2583. with patch("sys.stdout.isatty", return_value=False):
  2584. pager = cli.get_pager()
  2585. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2586. def test_pager_env_dulwich_pager(self):
  2587. """Test DULWICH_PAGER environment variable."""
  2588. os.environ["DULWICH_PAGER"] = "custom_pager"
  2589. with patch("sys.stdout.isatty", return_value=True):
  2590. pager = cli.get_pager()
  2591. self.assertIsInstance(pager, cli.Pager)
  2592. self.assertEqual(pager.pager_cmd, "custom_pager")
  2593. def test_pager_env_dulwich_pager_false(self):
  2594. """Test DULWICH_PAGER=false disables pager."""
  2595. os.environ["DULWICH_PAGER"] = "false"
  2596. with patch("sys.stdout.isatty", return_value=True):
  2597. pager = cli.get_pager()
  2598. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2599. def test_pager_env_git_pager(self):
  2600. """Test GIT_PAGER environment variable."""
  2601. os.environ["GIT_PAGER"] = "git_custom_pager"
  2602. with patch("sys.stdout.isatty", return_value=True):
  2603. pager = cli.get_pager()
  2604. self.assertIsInstance(pager, cli.Pager)
  2605. self.assertEqual(pager.pager_cmd, "git_custom_pager")
  2606. def test_pager_env_pager(self):
  2607. """Test PAGER environment variable."""
  2608. os.environ["PAGER"] = "my_pager"
  2609. with patch("sys.stdout.isatty", return_value=True):
  2610. pager = cli.get_pager()
  2611. self.assertIsInstance(pager, cli.Pager)
  2612. self.assertEqual(pager.pager_cmd, "my_pager")
  2613. def test_pager_env_priority(self):
  2614. """Test environment variable priority order."""
  2615. os.environ["PAGER"] = "pager_low"
  2616. os.environ["GIT_PAGER"] = "pager_medium"
  2617. os.environ["DULWICH_PAGER"] = "pager_high"
  2618. with patch("sys.stdout.isatty", return_value=True):
  2619. pager = cli.get_pager()
  2620. self.assertEqual(pager.pager_cmd, "pager_high")
  2621. def test_pager_config_core_pager(self):
  2622. """Test core.pager configuration."""
  2623. config = MagicMock()
  2624. config.get.return_value = b"config_pager"
  2625. with patch("sys.stdout.isatty", return_value=True):
  2626. pager = cli.get_pager(config=config)
  2627. self.assertIsInstance(pager, cli.Pager)
  2628. self.assertEqual(pager.pager_cmd, "config_pager")
  2629. config.get.assert_called_with(("core",), b"pager")
  2630. def test_pager_config_core_pager_false(self):
  2631. """Test core.pager=false disables pager."""
  2632. config = MagicMock()
  2633. config.get.return_value = b"false"
  2634. with patch("sys.stdout.isatty", return_value=True):
  2635. pager = cli.get_pager(config=config)
  2636. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2637. def test_pager_config_core_pager_empty(self):
  2638. """Test core.pager="" disables pager."""
  2639. config = MagicMock()
  2640. config.get.return_value = b""
  2641. with patch("sys.stdout.isatty", return_value=True):
  2642. pager = cli.get_pager(config=config)
  2643. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2644. def test_pager_config_per_command(self):
  2645. """Test per-command pager configuration."""
  2646. config = MagicMock()
  2647. config.get.side_effect = lambda section, key: {
  2648. (("pager",), b"log"): b"log_pager",
  2649. }.get((section, key), KeyError())
  2650. with patch("sys.stdout.isatty", return_value=True):
  2651. pager = cli.get_pager(config=config, cmd_name="log")
  2652. self.assertIsInstance(pager, cli.Pager)
  2653. self.assertEqual(pager.pager_cmd, "log_pager")
  2654. def test_pager_config_per_command_false(self):
  2655. """Test per-command pager=false disables pager."""
  2656. config = MagicMock()
  2657. config.get.return_value = b"false"
  2658. with patch("sys.stdout.isatty", return_value=True):
  2659. pager = cli.get_pager(config=config, cmd_name="log")
  2660. self.assertIsInstance(pager, cli._StreamContextAdapter)
  2661. def test_pager_config_per_command_true(self):
  2662. """Test per-command pager=true uses default pager."""
  2663. config = MagicMock()
  2664. def get_side_effect(section, key):
  2665. if section == ("pager",) and key == b"log":
  2666. return b"true"
  2667. raise KeyError
  2668. config.get.side_effect = get_side_effect
  2669. with patch("sys.stdout.isatty", return_value=True):
  2670. with patch("shutil.which", side_effect=lambda cmd: cmd == "less"):
  2671. pager = cli.get_pager(config=config, cmd_name="log")
  2672. self.assertIsInstance(pager, cli.Pager)
  2673. self.assertEqual(pager.pager_cmd, "less -FRX")
  2674. def test_pager_priority_order(self):
  2675. """Test complete priority order."""
  2676. # Set up all possible configurations
  2677. os.environ["PAGER"] = "env_pager"
  2678. os.environ["GIT_PAGER"] = "env_git_pager"
  2679. config = MagicMock()
  2680. def get_side_effect(section, key):
  2681. if section == ("pager",) and key == b"log":
  2682. return b"cmd_pager"
  2683. elif section == ("core",) and key == b"pager":
  2684. return b"core_pager"
  2685. raise KeyError
  2686. config.get.side_effect = get_side_effect
  2687. with patch("sys.stdout.isatty", return_value=True):
  2688. # Per-command config should win
  2689. pager = cli.get_pager(config=config, cmd_name="log")
  2690. self.assertEqual(pager.pager_cmd, "cmd_pager")
  2691. def test_pager_fallback_less(self):
  2692. """Test fallback to less with proper flags."""
  2693. with patch("sys.stdout.isatty", return_value=True):
  2694. with patch("shutil.which", side_effect=lambda cmd: cmd == "less"):
  2695. pager = cli.get_pager()
  2696. self.assertIsInstance(pager, cli.Pager)
  2697. self.assertEqual(pager.pager_cmd, "less -FRX")
  2698. def test_pager_fallback_more(self):
  2699. """Test fallback to more when less is not available."""
  2700. with patch("sys.stdout.isatty", return_value=True):
  2701. with patch("shutil.which", side_effect=lambda cmd: cmd == "more"):
  2702. pager = cli.get_pager()
  2703. self.assertIsInstance(pager, cli.Pager)
  2704. self.assertEqual(pager.pager_cmd, "more")
  2705. def test_pager_fallback_cat(self):
  2706. """Test ultimate fallback to cat."""
  2707. with patch("sys.stdout.isatty", return_value=True):
  2708. with patch("shutil.which", return_value=None):
  2709. pager = cli.get_pager()
  2710. self.assertIsInstance(pager, cli.Pager)
  2711. self.assertEqual(pager.pager_cmd, "cat")
  2712. def test_pager_context_manager(self):
  2713. """Test that pager works as a context manager."""
  2714. with patch("sys.stdout.isatty", return_value=True):
  2715. with cli.get_pager() as pager:
  2716. self.assertTrue(hasattr(pager, "write"))
  2717. self.assertTrue(hasattr(pager, "flush"))
  2718. class WorktreeCliTests(DulwichCliTestCase):
  2719. """Tests for worktree CLI commands."""
  2720. def setUp(self):
  2721. super().setUp()
  2722. # Base class already creates and initializes the repo
  2723. # Just create initial commit
  2724. with open(os.path.join(self.repo_path, "test.txt"), "w") as f:
  2725. f.write("test content")
  2726. from dulwich import porcelain
  2727. porcelain.add(self.repo_path, ["test.txt"])
  2728. porcelain.commit(self.repo_path, message=b"Initial commit")
  2729. def test_worktree_list(self):
  2730. """Test worktree list command."""
  2731. # Change to repo directory
  2732. old_cwd = os.getcwd()
  2733. os.chdir(self.repo_path)
  2734. try:
  2735. io.StringIO()
  2736. cmd = cli.cmd_worktree()
  2737. result = cmd.run(["list"])
  2738. # Should list the main worktree
  2739. self.assertEqual(result, 0)
  2740. finally:
  2741. os.chdir(old_cwd)
  2742. def test_worktree_add(self):
  2743. """Test worktree add command."""
  2744. wt_path = os.path.join(self.test_dir, "worktree1")
  2745. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2746. result, _stdout, _stderr = self._run_cli(
  2747. "worktree", "add", wt_path, "feature"
  2748. )
  2749. self.assertEqual(result, 0)
  2750. self.assertTrue(os.path.exists(wt_path))
  2751. log_output = "\n".join(cm.output)
  2752. self.assertIn("Worktree added:", log_output)
  2753. def test_worktree_add_detached(self):
  2754. """Test worktree add with detached HEAD."""
  2755. wt_path = os.path.join(self.test_dir, "detached-wt")
  2756. # Change to repo directory
  2757. old_cwd = os.getcwd()
  2758. os.chdir(self.repo_path)
  2759. try:
  2760. cmd = cli.cmd_worktree()
  2761. with patch("sys.stdout", new_callable=io.StringIO):
  2762. result = cmd.run(["add", "--detach", wt_path])
  2763. self.assertEqual(result, 0)
  2764. self.assertTrue(os.path.exists(wt_path))
  2765. finally:
  2766. os.chdir(old_cwd)
  2767. def test_worktree_remove(self):
  2768. """Test worktree remove command."""
  2769. # First add a worktree
  2770. wt_path = os.path.join(self.test_dir, "to-remove")
  2771. result, _stdout, _stderr = self._run_cli("worktree", "add", wt_path)
  2772. self.assertEqual(result, 0)
  2773. # Then remove it
  2774. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2775. result, _stdout, _stderr = self._run_cli("worktree", "remove", wt_path)
  2776. self.assertEqual(result, 0)
  2777. self.assertFalse(os.path.exists(wt_path))
  2778. log_output = "\n".join(cm.output)
  2779. self.assertIn("Worktree removed:", log_output)
  2780. def test_worktree_prune(self):
  2781. """Test worktree prune command."""
  2782. # Add a worktree and manually remove it
  2783. wt_path = os.path.join(self.test_dir, "to-prune")
  2784. result, _stdout, _stderr = self._run_cli("worktree", "add", wt_path)
  2785. self.assertEqual(result, 0)
  2786. shutil.rmtree(wt_path)
  2787. # Prune
  2788. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2789. result, _stdout, _stderr = self._run_cli("worktree", "prune", "-v")
  2790. self.assertEqual(result, 0)
  2791. log_output = "\n".join(cm.output)
  2792. self.assertIn("to-prune", log_output)
  2793. def test_worktree_lock_unlock(self):
  2794. """Test worktree lock and unlock commands."""
  2795. # Add a worktree
  2796. wt_path = os.path.join(self.test_dir, "lockable")
  2797. result, _stdout, _stderr = self._run_cli("worktree", "add", wt_path)
  2798. self.assertEqual(result, 0)
  2799. # Lock it
  2800. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2801. result, _stdout, _stderr = self._run_cli(
  2802. "worktree", "lock", wt_path, "--reason", "Testing"
  2803. )
  2804. self.assertEqual(result, 0)
  2805. log_output = "\n".join(cm.output)
  2806. self.assertIn("Worktree locked:", log_output)
  2807. # Unlock it
  2808. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2809. result, _stdout, _stderr = self._run_cli("worktree", "unlock", wt_path)
  2810. self.assertEqual(result, 0)
  2811. log_output = "\n".join(cm.output)
  2812. self.assertIn("Worktree unlocked:", log_output)
  2813. def test_worktree_move(self):
  2814. """Test worktree move command."""
  2815. # Add a worktree
  2816. old_path = os.path.join(self.test_dir, "old-location")
  2817. new_path = os.path.join(self.test_dir, "new-location")
  2818. result, _stdout, _stderr = self._run_cli("worktree", "add", old_path)
  2819. self.assertEqual(result, 0)
  2820. # Move it
  2821. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  2822. result, _stdout, _stderr = self._run_cli(
  2823. "worktree", "move", old_path, new_path
  2824. )
  2825. self.assertEqual(result, 0)
  2826. self.assertFalse(os.path.exists(old_path))
  2827. self.assertTrue(os.path.exists(new_path))
  2828. log_output = "\n".join(cm.output)
  2829. self.assertIn("Worktree moved:", log_output)
  2830. def test_worktree_invalid_command(self):
  2831. """Test invalid worktree subcommand."""
  2832. cmd = cli.cmd_worktree()
  2833. with patch("sys.stderr", new_callable=io.StringIO):
  2834. with self.assertRaises(SystemExit):
  2835. cmd.run(["invalid"])
  2836. class MergeBaseCommandTest(DulwichCliTestCase):
  2837. """Tests for merge-base command."""
  2838. def _create_commits(self):
  2839. """Helper to create a commit history for testing."""
  2840. # Create three commits in linear history
  2841. for i in range(1, 4):
  2842. test_file = os.path.join(self.repo_path, f"file{i}.txt")
  2843. with open(test_file, "w") as f:
  2844. f.write(f"content{i}")
  2845. self._run_cli("add", f"file{i}.txt")
  2846. self._run_cli("commit", f"--message=Commit {i}")
  2847. def test_merge_base_linear_history(self):
  2848. """Test merge-base with linear history."""
  2849. self._create_commits()
  2850. result, stdout, _stderr = self._run_cli("merge-base", "HEAD", "HEAD~1")
  2851. self.assertEqual(result, 0)
  2852. # Should return HEAD~1 as the merge base
  2853. output = stdout.strip()
  2854. # Verify it's a valid commit ID (40 hex chars)
  2855. self.assertEqual(len(output), 40)
  2856. self.assertTrue(all(c in "0123456789abcdef" for c in output))
  2857. def test_merge_base_is_ancestor_true(self):
  2858. """Test merge-base --is-ancestor when true."""
  2859. self._create_commits()
  2860. result, _stdout, _stderr = self._run_cli(
  2861. "merge-base", "--is-ancestor", "HEAD~1", "HEAD"
  2862. )
  2863. self.assertEqual(result, 0) # Exit code 0 means true
  2864. def test_merge_base_is_ancestor_false(self):
  2865. """Test merge-base --is-ancestor when false."""
  2866. self._create_commits()
  2867. result, _stdout, _stderr = self._run_cli(
  2868. "merge-base", "--is-ancestor", "HEAD", "HEAD~1"
  2869. )
  2870. self.assertEqual(result, 1) # Exit code 1 means false
  2871. def test_merge_base_independent(self):
  2872. """Test merge-base --independent."""
  2873. self._create_commits()
  2874. # All three commits in linear history - only HEAD should be independent
  2875. head = self.repo.refs[b"HEAD"]
  2876. head_1 = self.repo[head].parents[0]
  2877. head_2 = self.repo[head_1].parents[0]
  2878. result, stdout, _stderr = self._run_cli(
  2879. "merge-base",
  2880. "--independent",
  2881. head.decode(),
  2882. head_1.decode(),
  2883. head_2.decode(),
  2884. )
  2885. self.assertEqual(result, 0)
  2886. # Only HEAD should be in output (as it's the only independent commit)
  2887. lines = stdout.strip().split("\n")
  2888. self.assertEqual(len(lines), 1)
  2889. self.assertEqual(lines[0], head.decode())
  2890. def test_merge_base_requires_two_commits(self):
  2891. """Test merge-base requires at least two commits."""
  2892. self._create_commits()
  2893. result, _stdout, _stderr = self._run_cli("merge-base", "HEAD")
  2894. self.assertEqual(result, 1)
  2895. def test_merge_base_is_ancestor_requires_two_commits(self):
  2896. """Test merge-base --is-ancestor requires exactly two commits."""
  2897. self._create_commits()
  2898. result, _stdout, _stderr = self._run_cli("merge-base", "--is-ancestor", "HEAD")
  2899. self.assertEqual(result, 1)
  2900. class ConfigCommandTest(DulwichCliTestCase):
  2901. """Tests for config command."""
  2902. def test_config_set_and_get(self):
  2903. """Test setting and getting a config value."""
  2904. # Set a config value
  2905. result, stdout, _stderr = self._run_cli("config", "user.name", "Test User")
  2906. self.assertEqual(result, 0)
  2907. self.assertEqual(stdout, "")
  2908. # Get the value back
  2909. result, stdout, _stderr = self._run_cli("config", "user.name")
  2910. self.assertEqual(result, 0)
  2911. self.assertEqual(stdout, "Test User\n")
  2912. def test_config_set_and_get_subsection(self):
  2913. """Test setting and getting a config value with subsection."""
  2914. # Set a config value with subsection (e.g., remote.origin.url)
  2915. result, stdout, _stderr = self._run_cli(
  2916. "config", "remote.origin.url", "https://example.com/repo.git"
  2917. )
  2918. self.assertEqual(result, 0)
  2919. self.assertEqual(stdout, "")
  2920. # Get the value back
  2921. result, stdout, _stderr = self._run_cli("config", "remote.origin.url")
  2922. self.assertEqual(result, 0)
  2923. self.assertEqual(stdout, "https://example.com/repo.git\n")
  2924. def test_config_list(self):
  2925. """Test listing all config values."""
  2926. # Set some config values
  2927. self._run_cli("config", "user.name", "Test User")
  2928. self._run_cli("config", "user.email", "test@example.com")
  2929. # Get the actual config values that may vary by platform
  2930. config = self.repo.get_config()
  2931. filemode = config.get((b"core",), b"filemode")
  2932. try:
  2933. symlinks = config.get((b"core",), b"symlinks")
  2934. except KeyError:
  2935. symlinks = None
  2936. # List all values
  2937. result, stdout, _stderr = self._run_cli("config", "--list")
  2938. self.assertEqual(result, 0)
  2939. # Build expected output with platform-specific values
  2940. expected = "core.repositoryformatversion=0\n"
  2941. expected += f"core.filemode={filemode.decode('utf-8')}\n"
  2942. if symlinks is not None:
  2943. expected += f"core.symlinks={symlinks.decode('utf-8')}\n"
  2944. expected += (
  2945. "core.bare=false\n"
  2946. "core.logallrefupdates=true\n"
  2947. "user.name=Test User\n"
  2948. "user.email=test@example.com\n"
  2949. )
  2950. self.assertEqual(stdout, expected)
  2951. def test_config_unset(self):
  2952. """Test unsetting a config value."""
  2953. # Set a config value
  2954. self._run_cli("config", "user.name", "Test User")
  2955. # Verify it's set
  2956. result, stdout, _stderr = self._run_cli("config", "user.name")
  2957. self.assertEqual(result, 0)
  2958. self.assertEqual(stdout, "Test User\n")
  2959. # Unset it
  2960. result, stdout, _stderr = self._run_cli("config", "--unset", "user.name")
  2961. self.assertEqual(result, 0)
  2962. self.assertEqual(stdout, "")
  2963. # Verify it's gone
  2964. result, stdout, _stderr = self._run_cli("config", "user.name")
  2965. self.assertEqual(result, 1)
  2966. self.assertEqual(stdout, "")
  2967. def test_config_get_nonexistent(self):
  2968. """Test getting a nonexistent config value."""
  2969. result, stdout, _stderr = self._run_cli("config", "nonexistent.key")
  2970. self.assertEqual(result, 1)
  2971. self.assertEqual(stdout, "")
  2972. def test_config_unset_nonexistent(self):
  2973. """Test unsetting a nonexistent config value."""
  2974. result, _stdout, _stderr = self._run_cli("config", "--unset", "nonexistent.key")
  2975. self.assertEqual(result, 1)
  2976. def test_config_invalid_key_format(self):
  2977. """Test config with invalid key format."""
  2978. result, stdout, _stderr = self._run_cli("config", "invalidkey")
  2979. self.assertEqual(result, 1)
  2980. self.assertEqual(stdout, "")
  2981. def test_config_get_all(self):
  2982. """Test getting all values for a multivar."""
  2983. # Set multiple values for the same key
  2984. config = self.repo.get_config()
  2985. config.set(("test",), "multivar", "value1")
  2986. config.add(("test",), "multivar", "value2")
  2987. config.add(("test",), "multivar", "value3")
  2988. config.write_to_path()
  2989. # Get all values
  2990. result, stdout, _stderr = self._run_cli("config", "--get-all", "test.multivar")
  2991. self.assertEqual(result, 0)
  2992. self.assertEqual(stdout, "value1\nvalue2\nvalue3\n")
  2993. class GitFlushTest(TestCase):
  2994. """Tests for GIT_FLUSH environment variable support."""
  2995. def test_should_auto_flush_with_git_flush_1(self):
  2996. """Test that GIT_FLUSH=1 enables auto-flushing."""
  2997. mock_stream = MagicMock()
  2998. mock_stream.isatty.return_value = True
  2999. self.assertTrue(_should_auto_flush(mock_stream, env={"GIT_FLUSH": "1"}))
  3000. def test_should_auto_flush_with_git_flush_0(self):
  3001. """Test that GIT_FLUSH=0 disables auto-flushing."""
  3002. mock_stream = MagicMock()
  3003. mock_stream.isatty.return_value = True
  3004. self.assertFalse(_should_auto_flush(mock_stream, env={"GIT_FLUSH": "0"}))
  3005. def test_should_auto_flush_auto_detect_tty(self):
  3006. """Test that auto-detect returns False for TTY (no flush needed)."""
  3007. mock_stream = MagicMock()
  3008. mock_stream.isatty.return_value = True
  3009. self.assertFalse(_should_auto_flush(mock_stream, env={}))
  3010. def test_should_auto_flush_auto_detect_pipe(self):
  3011. """Test that auto-detect returns True for pipes (flush needed)."""
  3012. mock_stream = MagicMock()
  3013. mock_stream.isatty.return_value = False
  3014. self.assertTrue(_should_auto_flush(mock_stream, env={}))
  3015. def test_text_wrapper_flushes_on_write(self):
  3016. """Test that AutoFlushTextIOWrapper flushes after write."""
  3017. mock_stream = MagicMock()
  3018. wrapper = AutoFlushTextIOWrapper(mock_stream)
  3019. wrapper.write("test")
  3020. mock_stream.write.assert_called_once_with("test")
  3021. mock_stream.flush.assert_called_once()
  3022. def test_text_wrapper_flushes_on_writelines(self):
  3023. """Test that AutoFlushTextIOWrapper flushes after writelines."""
  3024. from dulwich.cli import AutoFlushTextIOWrapper
  3025. mock_stream = MagicMock()
  3026. wrapper = AutoFlushTextIOWrapper(mock_stream)
  3027. wrapper.writelines(["line1\n", "line2\n"])
  3028. mock_stream.writelines.assert_called_once()
  3029. mock_stream.flush.assert_called_once()
  3030. def test_binary_wrapper_flushes_on_write(self):
  3031. """Test that AutoFlushBinaryIOWrapper flushes after write."""
  3032. mock_stream = MagicMock()
  3033. wrapper = AutoFlushBinaryIOWrapper(mock_stream)
  3034. wrapper.write(b"test")
  3035. mock_stream.write.assert_called_once_with(b"test")
  3036. mock_stream.flush.assert_called_once()
  3037. def test_text_wrapper_env_classmethod(self):
  3038. """Test that AutoFlushTextIOWrapper.env() respects GIT_FLUSH."""
  3039. mock_stream = MagicMock()
  3040. mock_stream.isatty.return_value = False
  3041. wrapper = AutoFlushTextIOWrapper.env(mock_stream, env={"GIT_FLUSH": "1"})
  3042. self.assertIsInstance(wrapper, AutoFlushTextIOWrapper)
  3043. wrapper = AutoFlushTextIOWrapper.env(mock_stream, env={"GIT_FLUSH": "0"})
  3044. self.assertIs(mock_stream, wrapper)
  3045. def test_binary_wrapper_env_classmethod(self):
  3046. """Test that AutoFlushBinaryIOWrapper.env() respects GIT_FLUSH."""
  3047. mock_stream = MagicMock()
  3048. mock_stream.isatty.return_value = False
  3049. wrapper = AutoFlushBinaryIOWrapper.env(mock_stream, env={"GIT_FLUSH": "1"})
  3050. self.assertIsInstance(wrapper, AutoFlushBinaryIOWrapper)
  3051. wrapper = AutoFlushBinaryIOWrapper.env(mock_stream, env={"GIT_FLUSH": "0"})
  3052. self.assertIs(wrapper, mock_stream)
  3053. def test_wrapper_delegates_attributes(self):
  3054. """Test that wrapper delegates unknown attributes to stream."""
  3055. mock_stream = MagicMock()
  3056. mock_stream.encoding = "utf-8"
  3057. wrapper = AutoFlushTextIOWrapper(mock_stream)
  3058. self.assertEqual(wrapper.encoding, "utf-8")
  3059. def test_wrapper_context_manager(self):
  3060. """Test that wrapper supports context manager protocol."""
  3061. mock_stream = MagicMock()
  3062. wrapper = AutoFlushTextIOWrapper(mock_stream)
  3063. with wrapper as w:
  3064. self.assertIs(w, wrapper)
  3065. class MaintenanceCommandTest(DulwichCliTestCase):
  3066. """Tests for maintenance command."""
  3067. def setUp(self):
  3068. super().setUp()
  3069. # Set up a temporary HOME for testing global config
  3070. self.temp_home = tempfile.mkdtemp()
  3071. self.addCleanup(shutil.rmtree, self.temp_home)
  3072. self.overrideEnv("HOME", self.temp_home)
  3073. def test_maintenance_run_default(self):
  3074. """Test maintenance run with default tasks."""
  3075. result, _stdout, _stderr = self._run_cli("maintenance", "run")
  3076. self.assertIsNone(result)
  3077. def test_maintenance_run_specific_task(self):
  3078. """Test maintenance run with a specific task."""
  3079. result, _stdout, _stderr = self._run_cli(
  3080. "maintenance", "run", "--task", "pack-refs"
  3081. )
  3082. self.assertIsNone(result)
  3083. def test_maintenance_run_multiple_tasks(self):
  3084. """Test maintenance run with multiple specific tasks."""
  3085. result, _stdout, _stderr = self._run_cli(
  3086. "maintenance", "run", "--task", "pack-refs", "--task", "gc"
  3087. )
  3088. self.assertIsNone(result)
  3089. def test_maintenance_run_quiet(self):
  3090. """Test maintenance run with quiet flag."""
  3091. result, _stdout, _stderr = self._run_cli("maintenance", "run", "--quiet")
  3092. self.assertIsNone(result)
  3093. def test_maintenance_run_auto(self):
  3094. """Test maintenance run with auto flag."""
  3095. result, _stdout, _stderr = self._run_cli("maintenance", "run", "--auto")
  3096. self.assertIsNone(result)
  3097. def test_maintenance_no_subcommand(self):
  3098. """Test maintenance command without subcommand shows help."""
  3099. result, _stdout, _stderr = self._run_cli("maintenance")
  3100. self.assertEqual(result, 1)
  3101. def test_maintenance_register(self):
  3102. """Test maintenance register subcommand."""
  3103. result, _stdout, _stderr = self._run_cli("maintenance", "register")
  3104. self.assertIsNone(result)
  3105. def test_maintenance_unregister(self):
  3106. """Test maintenance unregister subcommand."""
  3107. # First register
  3108. _result, _stdout, _stderr = self._run_cli("maintenance", "register")
  3109. # Then unregister
  3110. result, _stdout, _stderr = self._run_cli("maintenance", "unregister")
  3111. self.assertIsNone(result)
  3112. def test_maintenance_unregister_not_registered(self):
  3113. """Test unregistering a repository that is not registered."""
  3114. result, _stdout, _stderr = self._run_cli("maintenance", "unregister")
  3115. self.assertEqual(result, 1)
  3116. def test_maintenance_unregister_force(self):
  3117. """Test unregistering with --force flag."""
  3118. result, _stdout, _stderr = self._run_cli("maintenance", "unregister", "--force")
  3119. self.assertIsNone(result)
  3120. def test_maintenance_unimplemented_subcommand(self):
  3121. """Test unimplemented maintenance subcommands."""
  3122. for subcommand in ["start", "stop"]:
  3123. result, _stdout, _stderr = self._run_cli("maintenance", subcommand)
  3124. self.assertEqual(result, 1)
  3125. class InterpretTrailersCommandTest(DulwichCliTestCase):
  3126. """Tests for interpret-trailers command."""
  3127. def test_parse_trailers_from_file(self):
  3128. """Test parsing trailers from a file."""
  3129. # Create a message file with trailers
  3130. msg_file = os.path.join(self.test_dir, "message.txt")
  3131. with open(msg_file, "wb") as f:
  3132. f.write(b"Subject\n\nBody\n\nSigned-off-by: Alice <alice@example.com>\n")
  3133. result, stdout, _stderr = self._run_cli(
  3134. "interpret-trailers", "--only-trailers", msg_file
  3135. )
  3136. self.assertIsNone(result)
  3137. self.assertIn("Signed-off-by: Alice <alice@example.com>", stdout)
  3138. def test_add_trailer_to_message(self):
  3139. """Test adding a trailer to a message."""
  3140. msg_file = os.path.join(self.test_dir, "message.txt")
  3141. with open(msg_file, "wb") as f:
  3142. f.write(b"Subject\n\nBody text\n")
  3143. result, stdout, _stderr = self._run_cli(
  3144. "interpret-trailers",
  3145. "--trailer",
  3146. "Signed-off-by:Alice <alice@example.com>",
  3147. msg_file,
  3148. )
  3149. self.assertIsNone(result)
  3150. self.assertIn("Signed-off-by: Alice <alice@example.com>", stdout)
  3151. self.assertIn("Subject", stdout)
  3152. self.assertIn("Body text", stdout)
  3153. def test_add_multiple_trailers(self):
  3154. """Test adding multiple trailers."""
  3155. msg_file = os.path.join(self.test_dir, "message.txt")
  3156. with open(msg_file, "wb") as f:
  3157. f.write(b"Subject\n\nBody\n")
  3158. result, stdout, _stderr = self._run_cli(
  3159. "interpret-trailers",
  3160. "--trailer",
  3161. "Signed-off-by:Alice",
  3162. "--trailer",
  3163. "Reviewed-by:Bob",
  3164. msg_file,
  3165. )
  3166. self.assertIsNone(result)
  3167. self.assertIn("Signed-off-by: Alice", stdout)
  3168. self.assertIn("Reviewed-by: Bob", stdout)
  3169. def test_parse_shorthand(self):
  3170. """Test --parse shorthand option."""
  3171. msg_file = os.path.join(self.test_dir, "message.txt")
  3172. with open(msg_file, "wb") as f:
  3173. f.write(b"Subject\n\nBody\n\nSigned-off-by: Alice\n")
  3174. result, stdout, _stderr = self._run_cli(
  3175. "interpret-trailers", "--parse", msg_file
  3176. )
  3177. self.assertIsNone(result)
  3178. # --parse is shorthand for --only-trailers --only-input --unfold
  3179. self.assertEqual(stdout, "Signed-off-by: Alice\n")
  3180. def test_trim_empty(self):
  3181. """Test --trim-empty option."""
  3182. msg_file = os.path.join(self.test_dir, "message.txt")
  3183. with open(msg_file, "wb") as f:
  3184. f.write(b"Subject\n\nBody\n\nSigned-off-by: Alice\nReviewed-by: \n")
  3185. result, stdout, _stderr = self._run_cli(
  3186. "interpret-trailers", "--trim-empty", "--only-trailers", msg_file
  3187. )
  3188. self.assertIsNone(result)
  3189. self.assertIn("Signed-off-by: Alice", stdout)
  3190. self.assertNotIn("Reviewed-by:", stdout)
  3191. def test_if_exists_replace(self):
  3192. """Test --if-exists replace option."""
  3193. msg_file = os.path.join(self.test_dir, "message.txt")
  3194. with open(msg_file, "wb") as f:
  3195. f.write(b"Subject\n\nBody\n\nSigned-off-by: Alice\n")
  3196. result, stdout, _stderr = self._run_cli(
  3197. "interpret-trailers",
  3198. "--if-exists",
  3199. "replace",
  3200. "--trailer",
  3201. "Signed-off-by:Bob",
  3202. msg_file,
  3203. )
  3204. self.assertIsNone(result)
  3205. self.assertIn("Signed-off-by: Bob", stdout)
  3206. self.assertNotIn("Alice", stdout)
  3207. def test_trailer_with_equals(self):
  3208. """Test trailer with equals separator."""
  3209. msg_file = os.path.join(self.test_dir, "message.txt")
  3210. with open(msg_file, "wb") as f:
  3211. f.write(b"Subject\n\nBody\n")
  3212. result, stdout, _stderr = self._run_cli(
  3213. "interpret-trailers", "--trailer", "Bug=12345", msg_file
  3214. )
  3215. self.assertIsNone(result)
  3216. self.assertIn("Bug: 12345", stdout)
  3217. class ReplaceCommandTest(DulwichCliTestCase):
  3218. """Tests for replace command."""
  3219. def test_replace_create(self):
  3220. """Test creating a replacement ref."""
  3221. # Create two commits
  3222. [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
  3223. self.repo[b"HEAD"] = c1.id
  3224. # Create replacement using the create form (decode to string for CLI)
  3225. c1_str = c1.id.decode("ascii")
  3226. c2_str = c2.id.decode("ascii")
  3227. _result, _stdout, _stderr = self._run_cli("replace", c1_str, c2_str)
  3228. # Verify the replacement ref was created
  3229. replace_ref = b"refs/replace/" + c1.id
  3230. self.assertIn(replace_ref, self.repo.refs.keys())
  3231. self.assertEqual(c2.id, self.repo.refs[replace_ref])
  3232. def test_replace_list_empty(self):
  3233. """Test listing replacements when there are none."""
  3234. _result, stdout, _stderr = self._run_cli("replace", "list")
  3235. self.assertEqual("", stdout)
  3236. def test_replace_list(self):
  3237. """Test listing replacement refs."""
  3238. # Create two commits
  3239. [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
  3240. self.repo[b"HEAD"] = c1.id
  3241. # Create replacement
  3242. c1_str = c1.id.decode("ascii")
  3243. c2_str = c2.id.decode("ascii")
  3244. self._run_cli("replace", c1_str, c2_str)
  3245. # List replacements
  3246. _result, stdout, _stderr = self._run_cli("replace", "list")
  3247. self.assertIn(c1_str, stdout)
  3248. self.assertIn(c2_str, stdout)
  3249. def test_replace_default_list(self):
  3250. """Test that replace without subcommand defaults to list."""
  3251. # Create two commits
  3252. [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
  3253. self.repo[b"HEAD"] = c1.id
  3254. # Create replacement
  3255. c1_str = c1.id.decode("ascii")
  3256. c2_str = c2.id.decode("ascii")
  3257. self._run_cli("replace", c1_str, c2_str)
  3258. # Call replace without subcommand (should list)
  3259. _result, stdout, _stderr = self._run_cli("replace")
  3260. self.assertIn(c1_str, stdout)
  3261. self.assertIn(c2_str, stdout)
  3262. def test_replace_delete(self):
  3263. """Test deleting a replacement ref."""
  3264. # Create two commits
  3265. [c1, c2] = build_commit_graph(self.repo.object_store, [[1], [2]])
  3266. self.repo[b"HEAD"] = c1.id
  3267. # Create replacement
  3268. c1_str = c1.id.decode("ascii")
  3269. c2_str = c2.id.decode("ascii")
  3270. self._run_cli("replace", c1_str, c2_str)
  3271. # Verify it exists
  3272. replace_ref = b"refs/replace/" + c1.id
  3273. self.assertIn(replace_ref, self.repo.refs.keys())
  3274. # Delete the replacement
  3275. _result, _stdout, _stderr = self._run_cli("replace", "delete", c1_str)
  3276. # Verify it's gone
  3277. self.assertNotIn(replace_ref, self.repo.refs.keys())
  3278. def test_replace_delete_nonexistent(self):
  3279. """Test deleting a nonexistent replacement ref fails."""
  3280. # Create a commit
  3281. [c1] = build_commit_graph(self.repo.object_store, [[1]])
  3282. self.repo[b"HEAD"] = c1.id
  3283. # Try to delete a non-existent replacement
  3284. c1_str = c1.id.decode("ascii")
  3285. result, _stdout, _stderr = self._run_cli("replace", "delete", c1_str)
  3286. self.assertEqual(result, 1)
  3287. class StripspaceCommandTest(DulwichCliTestCase):
  3288. """Tests for stripspace command."""
  3289. def test_stripspace_from_file(self):
  3290. """Test stripspace from a file."""
  3291. # Create a text file with whitespace issues
  3292. text_file = os.path.join(self.test_dir, "message.txt")
  3293. with open(text_file, "wb") as f:
  3294. f.write(b" hello \n\n\n\n world \n\n")
  3295. result, stdout, _stderr = self._run_cli("stripspace", text_file)
  3296. self.assertIsNone(result)
  3297. self.assertEqual(stdout, "hello\n\nworld\n")
  3298. def test_stripspace_simple(self):
  3299. """Test basic stripspace functionality."""
  3300. text_file = os.path.join(self.test_dir, "message.txt")
  3301. with open(text_file, "wb") as f:
  3302. f.write(b"hello\nworld\n")
  3303. result, stdout, _stderr = self._run_cli("stripspace", text_file)
  3304. self.assertIsNone(result)
  3305. self.assertEqual(stdout, "hello\nworld\n")
  3306. def test_stripspace_trailing_whitespace(self):
  3307. """Test that trailing whitespace is removed."""
  3308. text_file = os.path.join(self.test_dir, "message.txt")
  3309. with open(text_file, "wb") as f:
  3310. f.write(b"hello \nworld\t\n")
  3311. result, stdout, _stderr = self._run_cli("stripspace", text_file)
  3312. self.assertIsNone(result)
  3313. self.assertEqual(stdout, "hello\nworld\n")
  3314. def test_stripspace_strip_comments(self):
  3315. """Test stripping comments."""
  3316. text_file = os.path.join(self.test_dir, "message.txt")
  3317. with open(text_file, "wb") as f:
  3318. f.write(b"# comment\nhello\n# another comment\nworld\n")
  3319. result, stdout, _stderr = self._run_cli(
  3320. "stripspace", "--strip-comments", text_file
  3321. )
  3322. self.assertIsNone(result)
  3323. self.assertEqual(stdout, "hello\nworld\n")
  3324. def test_stripspace_comment_lines(self):
  3325. """Test prepending comment character."""
  3326. text_file = os.path.join(self.test_dir, "message.txt")
  3327. with open(text_file, "wb") as f:
  3328. f.write(b"hello\nworld\n")
  3329. result, stdout, _stderr = self._run_cli(
  3330. "stripspace", "--comment-lines", text_file
  3331. )
  3332. self.assertIsNone(result)
  3333. self.assertEqual(stdout, "# hello\n# world\n")
  3334. def test_stripspace_custom_comment_char(self):
  3335. """Test using custom comment character."""
  3336. text_file = os.path.join(self.test_dir, "message.txt")
  3337. with open(text_file, "wb") as f:
  3338. f.write(b"; comment\nhello\n; another comment\nworld\n")
  3339. result, stdout, _stderr = self._run_cli(
  3340. "stripspace", "--strip-comments", "--comment-char", ";", text_file
  3341. )
  3342. self.assertIsNone(result)
  3343. self.assertEqual(stdout, "hello\nworld\n")
  3344. def test_stripspace_leading_trailing_blanks(self):
  3345. """Test removing leading and trailing blank lines."""
  3346. text_file = os.path.join(self.test_dir, "message.txt")
  3347. with open(text_file, "wb") as f:
  3348. f.write(b"\n\nhello\nworld\n\n\n")
  3349. result, stdout, _stderr = self._run_cli("stripspace", text_file)
  3350. self.assertIsNone(result)
  3351. self.assertEqual(stdout, "hello\nworld\n")
  3352. def test_stripspace_collapse_blank_lines(self):
  3353. """Test collapsing multiple blank lines."""
  3354. text_file = os.path.join(self.test_dir, "message.txt")
  3355. with open(text_file, "wb") as f:
  3356. f.write(b"hello\n\n\n\nworld\n")
  3357. result, stdout, _stderr = self._run_cli("stripspace", text_file)
  3358. self.assertIsNone(result)
  3359. self.assertEqual(stdout, "hello\n\nworld\n")
  3360. class ColumnCommandTest(DulwichCliTestCase):
  3361. """Tests for column command."""
  3362. def test_column_mode_default(self):
  3363. """Test column mode (default) - fills columns first."""
  3364. old_stdin = sys.stdin
  3365. try:
  3366. sys.stdin = io.StringIO("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n")
  3367. result, stdout, _stderr = self._run_cli("column", "--width", "40")
  3368. self.assertIsNone(result)
  3369. # In column mode, items go down then across
  3370. # With 12 items and width 40, should fit in multiple columns
  3371. lines = stdout.strip().split("\n")
  3372. # First line should start with "1"
  3373. self.assertTrue(lines[0].startswith("1"))
  3374. finally:
  3375. sys.stdin = old_stdin
  3376. def test_column_mode_row(self):
  3377. """Test row mode - fills rows first."""
  3378. old_stdin = sys.stdin
  3379. try:
  3380. sys.stdin = io.StringIO("1\n2\n3\n4\n5\n6\n")
  3381. result, stdout, _stderr = self._run_cli(
  3382. "column", "--mode", "row", "--width", "40"
  3383. )
  3384. self.assertIsNone(result)
  3385. # In row mode, items go across then down
  3386. # Should have items 1, 2, 3... on first line
  3387. lines = stdout.strip().split("\n")
  3388. self.assertTrue("1" in lines[0])
  3389. self.assertTrue("2" in lines[0])
  3390. finally:
  3391. sys.stdin = old_stdin
  3392. def test_column_mode_plain(self):
  3393. """Test plain mode - one item per line."""
  3394. old_stdin = sys.stdin
  3395. try:
  3396. sys.stdin = io.StringIO("apple\nbanana\ncherry\n")
  3397. result, stdout, _stderr = self._run_cli("column", "--mode", "plain")
  3398. self.assertIsNone(result)
  3399. self.assertEqual(stdout, "apple\nbanana\ncherry\n")
  3400. finally:
  3401. sys.stdin = old_stdin
  3402. def test_column_padding(self):
  3403. """Test custom padding between columns."""
  3404. old_stdin = sys.stdin
  3405. try:
  3406. sys.stdin = io.StringIO("a\nb\nc\nd\ne\nf\n")
  3407. result, stdout, _stderr = self._run_cli(
  3408. "column", "--mode", "row", "--padding", "5", "--width", "80"
  3409. )
  3410. self.assertIsNone(result)
  3411. # With padding=5, should have 5 spaces between items
  3412. self.assertIn(" ", stdout)
  3413. finally:
  3414. sys.stdin = old_stdin
  3415. def test_column_indent(self):
  3416. """Test indent prepended to each line."""
  3417. old_stdin = sys.stdin
  3418. try:
  3419. sys.stdin = io.StringIO("apple\nbanana\n")
  3420. result, stdout, _stderr = self._run_cli(
  3421. "column", "--mode", "plain", "--indent", " "
  3422. )
  3423. self.assertIsNone(result)
  3424. lines = stdout.split("\n")
  3425. self.assertTrue(lines[0].startswith(" apple"))
  3426. self.assertTrue(lines[1].startswith(" banana"))
  3427. finally:
  3428. sys.stdin = old_stdin
  3429. def test_column_empty_input(self):
  3430. """Test with empty input."""
  3431. old_stdin = sys.stdin
  3432. try:
  3433. sys.stdin = io.StringIO("")
  3434. result, stdout, _stderr = self._run_cli("column")
  3435. self.assertIsNone(result)
  3436. self.assertEqual(stdout, "")
  3437. finally:
  3438. sys.stdin = old_stdin
  3439. def test_column_single_item(self):
  3440. """Test with single item."""
  3441. old_stdin = sys.stdin
  3442. try:
  3443. sys.stdin = io.StringIO("single\n")
  3444. result, stdout, _stderr = self._run_cli("column")
  3445. self.assertIsNone(result)
  3446. self.assertEqual(stdout, "single\n")
  3447. finally:
  3448. sys.stdin = old_stdin
  3449. class MailinfoCommandTests(DulwichCliTestCase):
  3450. """Tests for the mailinfo command."""
  3451. def test_mailinfo_basic(self):
  3452. """Test basic mailinfo command."""
  3453. email_content = b"""From: Test User <test@example.com>
  3454. Subject: [PATCH] Add feature
  3455. Date: Mon, 1 Jan 2024 12:00:00 +0000
  3456. This is the commit message.
  3457. ---
  3458. diff --git a/file.txt b/file.txt
  3459. """
  3460. email_file = os.path.join(self.test_dir, "email.txt")
  3461. with open(email_file, "wb") as f:
  3462. f.write(email_content)
  3463. msg_file = os.path.join(self.test_dir, "msg")
  3464. patch_file = os.path.join(self.test_dir, "patch")
  3465. result, stdout, _stderr = self._run_cli(
  3466. "mailinfo", msg_file, patch_file, email_file
  3467. )
  3468. self.assertIsNone(result)
  3469. # Check stdout contains author info
  3470. self.assertIn("Author: Test User", stdout)
  3471. self.assertIn("Email: test@example.com", stdout)
  3472. self.assertIn("Subject: Add feature", stdout)
  3473. # Check files were written
  3474. self.assertTrue(os.path.exists(msg_file))
  3475. self.assertTrue(os.path.exists(patch_file))
  3476. # Check file contents
  3477. with open(msg_file) as f:
  3478. msg_content = f.read()
  3479. self.assertIn("This is the commit message.", msg_content)
  3480. with open(patch_file) as f:
  3481. patch_content = f.read()
  3482. self.assertIn("diff --git", patch_content)
  3483. def test_mailinfo_keep_subject(self):
  3484. """Test mailinfo with -k flag."""
  3485. email_content = b"""From: Test <test@example.com>
  3486. Subject: [PATCH 1/2] Feature
  3487. Body
  3488. """
  3489. email_file = os.path.join(self.test_dir, "email.txt")
  3490. with open(email_file, "wb") as f:
  3491. f.write(email_content)
  3492. msg_file = os.path.join(self.test_dir, "msg")
  3493. patch_file = os.path.join(self.test_dir, "patch")
  3494. result, stdout, _stderr = self._run_cli(
  3495. "mailinfo", "-k", msg_file, patch_file, email_file
  3496. )
  3497. self.assertIsNone(result)
  3498. self.assertIn("Subject: [PATCH 1/2] Feature", stdout)
  3499. def test_mailinfo_keep_non_patch(self):
  3500. """Test mailinfo with -b flag."""
  3501. email_content = b"""From: Test <test@example.com>
  3502. Subject: [RFC][PATCH] Feature
  3503. Body
  3504. """
  3505. email_file = os.path.join(self.test_dir, "email.txt")
  3506. with open(email_file, "wb") as f:
  3507. f.write(email_content)
  3508. msg_file = os.path.join(self.test_dir, "msg")
  3509. patch_file = os.path.join(self.test_dir, "patch")
  3510. result, stdout, _stderr = self._run_cli(
  3511. "mailinfo", "-b", msg_file, patch_file, email_file
  3512. )
  3513. self.assertIsNone(result)
  3514. self.assertIn("Subject: [RFC] Feature", stdout)
  3515. def test_mailinfo_scissors(self):
  3516. """Test mailinfo with --scissors flag."""
  3517. email_content = b"""From: Test <test@example.com>
  3518. Subject: Test
  3519. Ignore this part
  3520. -- >8 --
  3521. Keep this part
  3522. """
  3523. email_file = os.path.join(self.test_dir, "email.txt")
  3524. with open(email_file, "wb") as f:
  3525. f.write(email_content)
  3526. msg_file = os.path.join(self.test_dir, "msg")
  3527. patch_file = os.path.join(self.test_dir, "patch")
  3528. result, _stdout, _stderr = self._run_cli(
  3529. "mailinfo", "--scissors", msg_file, patch_file, email_file
  3530. )
  3531. self.assertIsNone(result)
  3532. # Check message file
  3533. with open(msg_file) as f:
  3534. msg_content = f.read()
  3535. self.assertIn("Keep this part", msg_content)
  3536. self.assertNotIn("Ignore this part", msg_content)
  3537. def test_mailinfo_message_id(self):
  3538. """Test mailinfo with -m flag."""
  3539. email_content = b"""From: Test <test@example.com>
  3540. Subject: Test
  3541. Message-ID: <test123@example.com>
  3542. Body
  3543. """
  3544. email_file = os.path.join(self.test_dir, "email.txt")
  3545. with open(email_file, "wb") as f:
  3546. f.write(email_content)
  3547. msg_file = os.path.join(self.test_dir, "msg")
  3548. patch_file = os.path.join(self.test_dir, "patch")
  3549. result, _stdout, _stderr = self._run_cli(
  3550. "mailinfo", "-m", msg_file, patch_file, email_file
  3551. )
  3552. self.assertIsNone(result)
  3553. # Check message file contains Message-ID
  3554. with open(msg_file) as f:
  3555. msg_content = f.read()
  3556. self.assertIn("Message-ID:", msg_content)
  3557. def test_mailinfo_encoding(self):
  3558. """Test mailinfo with --encoding flag."""
  3559. email_content = (
  3560. b"From: Test <test@example.com>\n"
  3561. b"Subject: Test\n"
  3562. b"Content-Type: text/plain; charset=utf-8\n"
  3563. b"\n"
  3564. b"Body with UTF-8: " + "naïve".encode() + b"\n"
  3565. )
  3566. email_file = os.path.join(self.test_dir, "email.txt")
  3567. with open(email_file, "wb") as f:
  3568. f.write(email_content)
  3569. msg_file = os.path.join(self.test_dir, "msg")
  3570. patch_file = os.path.join(self.test_dir, "patch")
  3571. result, _stdout, _stderr = self._run_cli(
  3572. "mailinfo", "--encoding", "utf-8", msg_file, patch_file, email_file
  3573. )
  3574. self.assertIsNone(result)
  3575. # Just verify the command runs successfully
  3576. with open(msg_file) as f:
  3577. msg_content = f.read()
  3578. self.assertIn("Body", msg_content)
  3579. class DiagnoseCommandTest(DulwichCliTestCase):
  3580. """Tests for diagnose command."""
  3581. def test_diagnose(self):
  3582. """Test the diagnose command."""
  3583. with self.assertLogs("dulwich.cli", level="INFO") as cm:
  3584. result, _stdout, _stderr = self._run_cli("diagnose")
  3585. self.assertIsNone(result)
  3586. # Check that key information is present in log output
  3587. log_output = "\n".join(cm.output)
  3588. self.assertIn("Python version:", log_output)
  3589. self.assertIn("Python executable:", log_output)
  3590. self.assertIn("PYTHONPATH:", log_output)
  3591. self.assertIn("sys.path:", log_output)
  3592. self.assertIn("Dulwich version:", log_output)
  3593. self.assertIn("Installed dependencies:", log_output)
  3594. # Check that at least core dependencies are listed
  3595. self.assertIn("urllib3:", log_output)
  3596. if __name__ == "__main__":
  3597. unittest.main()