cli.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418
  1. #
  2. # dulwich - Simple command-line interface to Dulwich
  3. # Copyright (C) 2008-2011 Jelmer Vernooij <jelmer@jelmer.uk>
  4. # vim: expandtab
  5. #
  6. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  7. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  8. # General Public License as public by the Free Software Foundation; version 2.0
  9. # or (at your option) any later version. You can redistribute it and/or
  10. # modify it under the terms of either of these two licenses.
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. #
  18. # You should have received a copy of the licenses; if not, see
  19. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  20. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  21. # License, Version 2.0.
  22. #
  23. """Simple command-line interface to Dulwich>.
  24. This is a very simple command-line wrapper for Dulwich. It is by
  25. no means intended to be a full-blown Git command-line interface but just
  26. a way to test Dulwich.
  27. """
  28. import argparse
  29. import os
  30. import signal
  31. import sys
  32. from pathlib import Path
  33. from typing import TYPE_CHECKING, ClassVar, Optional
  34. from dulwich import porcelain
  35. from .client import GitProtocolError, get_transport_and_path
  36. from .errors import ApplyDeltaError
  37. from .index import Index
  38. from .objectspec import parse_commit
  39. from .pack import Pack, sha_to_hex
  40. from .repo import Repo
  41. if TYPE_CHECKING:
  42. pass
  43. def signal_int(signal, frame) -> None:
  44. sys.exit(1)
  45. def signal_quit(signal, frame) -> None:
  46. import pdb
  47. pdb.set_trace()
  48. def parse_relative_time(time_str):
  49. """Parse a relative time string like '2 weeks ago' into seconds.
  50. Args:
  51. time_str: String like '2 weeks ago' or 'now'
  52. Returns:
  53. Number of seconds
  54. Raises:
  55. ValueError: If the time string cannot be parsed
  56. """
  57. if time_str == "now":
  58. return 0
  59. if not time_str.endswith(" ago"):
  60. raise ValueError(f"Invalid relative time format: {time_str}")
  61. parts = time_str[:-4].split()
  62. if len(parts) != 2:
  63. raise ValueError(f"Invalid relative time format: {time_str}")
  64. try:
  65. num = int(parts[0])
  66. unit = parts[1]
  67. multipliers = {
  68. "second": 1,
  69. "seconds": 1,
  70. "minute": 60,
  71. "minutes": 60,
  72. "hour": 3600,
  73. "hours": 3600,
  74. "day": 86400,
  75. "days": 86400,
  76. "week": 604800,
  77. "weeks": 604800,
  78. }
  79. if unit in multipliers:
  80. return num * multipliers[unit]
  81. else:
  82. raise ValueError(f"Unknown time unit: {unit}")
  83. except ValueError as e:
  84. if "invalid literal" in str(e):
  85. raise ValueError(f"Invalid number in relative time: {parts[0]}")
  86. raise
  87. def format_bytes(bytes):
  88. """Format bytes as human-readable string.
  89. Args:
  90. bytes: Number of bytes
  91. Returns:
  92. Human-readable string like "1.5 MB"
  93. """
  94. for unit in ["B", "KB", "MB", "GB"]:
  95. if bytes < 1024.0:
  96. return f"{bytes:.1f} {unit}"
  97. bytes /= 1024.0
  98. return f"{bytes:.1f} TB"
  99. class Command:
  100. """A Dulwich subcommand."""
  101. def run(self, args) -> Optional[int]:
  102. """Run the command."""
  103. raise NotImplementedError(self.run)
  104. class cmd_archive(Command):
  105. def run(self, args) -> None:
  106. parser = argparse.ArgumentParser()
  107. parser.add_argument(
  108. "--remote",
  109. type=str,
  110. help="Retrieve archive from specified remote repo",
  111. )
  112. parser.add_argument("committish", type=str, nargs="?")
  113. args = parser.parse_args(args)
  114. if args.remote:
  115. client, path = get_transport_and_path(args.remote)
  116. client.archive(
  117. path,
  118. args.committish,
  119. sys.stdout.write,
  120. write_error=sys.stderr.write,
  121. )
  122. else:
  123. # Use buffer if available (for binary output), otherwise use stdout
  124. outstream = getattr(sys.stdout, "buffer", sys.stdout)
  125. porcelain.archive(
  126. ".", args.committish, outstream=outstream, errstream=sys.stderr
  127. )
  128. class cmd_add(Command):
  129. def run(self, argv) -> None:
  130. parser = argparse.ArgumentParser()
  131. parser.add_argument("path", nargs="+")
  132. args = parser.parse_args(argv)
  133. # Convert '.' to None to add all files
  134. paths = args.path
  135. if len(paths) == 1 and paths[0] == ".":
  136. paths = None
  137. porcelain.add(".", paths=paths)
  138. class cmd_rm(Command):
  139. def run(self, argv) -> None:
  140. parser = argparse.ArgumentParser()
  141. parser.add_argument(
  142. "--cached", action="store_true", help="Remove from index only"
  143. )
  144. parser.add_argument("path", type=Path, nargs="+")
  145. args = parser.parse_args(argv)
  146. porcelain.remove(".", paths=args.path, cached=args.cached)
  147. class cmd_fetch_pack(Command):
  148. def run(self, argv) -> None:
  149. parser = argparse.ArgumentParser()
  150. parser.add_argument("--all", action="store_true")
  151. parser.add_argument("location", nargs="?", type=str)
  152. parser.add_argument("refs", nargs="*", type=str)
  153. args = parser.parse_args(argv)
  154. client, path = get_transport_and_path(args.location)
  155. r = Repo(".")
  156. if args.all:
  157. determine_wants = r.object_store.determine_wants_all
  158. else:
  159. def determine_wants(refs, depth: Optional[int] = None):
  160. return [y.encode("utf-8") for y in args.refs if y not in r.object_store]
  161. client.fetch(path, r, determine_wants)
  162. class cmd_fetch(Command):
  163. def run(self, args) -> None:
  164. parser = argparse.ArgumentParser()
  165. parser.add_argument("location", help="Remote location to fetch from")
  166. args = parser.parse_args(args)
  167. client, path = get_transport_and_path(args.location)
  168. r = Repo(".")
  169. def progress(msg: bytes) -> None:
  170. sys.stdout.buffer.write(msg)
  171. refs = client.fetch(path, r, progress=progress)
  172. print("Remote refs:")
  173. for item in refs.items():
  174. print("{} -> {}".format(*item))
  175. class cmd_for_each_ref(Command):
  176. def run(self, args) -> None:
  177. parser = argparse.ArgumentParser()
  178. parser.add_argument("pattern", type=str, nargs="?")
  179. args = parser.parse_args(args)
  180. for sha, object_type, ref in porcelain.for_each_ref(".", args.pattern):
  181. print(f"{sha.decode()} {object_type.decode()}\t{ref.decode()}")
  182. class cmd_fsck(Command):
  183. def run(self, args) -> None:
  184. parser = argparse.ArgumentParser()
  185. parser.parse_args(args)
  186. for obj, msg in porcelain.fsck("."):
  187. print(f"{obj}: {msg}")
  188. class cmd_log(Command):
  189. def run(self, args) -> None:
  190. parser = argparse.ArgumentParser()
  191. parser.add_argument(
  192. "--reverse",
  193. action="store_true",
  194. help="Reverse order in which entries are printed",
  195. )
  196. parser.add_argument(
  197. "--name-status",
  198. action="store_true",
  199. help="Print name/status for each changed file",
  200. )
  201. parser.add_argument("paths", nargs="*", help="Paths to show log for")
  202. args = parser.parse_args(args)
  203. porcelain.log(
  204. ".",
  205. paths=args.paths,
  206. reverse=args.reverse,
  207. name_status=args.name_status,
  208. outstream=sys.stdout,
  209. )
  210. class cmd_diff(Command):
  211. def run(self, args) -> None:
  212. parser = argparse.ArgumentParser()
  213. parser.add_argument(
  214. "commit", nargs="?", default="HEAD", help="Commit to show diff for"
  215. )
  216. args = parser.parse_args(args)
  217. r = Repo(".")
  218. commit_id = (
  219. args.commit.encode() if isinstance(args.commit, str) else args.commit
  220. )
  221. commit = parse_commit(r, commit_id)
  222. parent_commit = r[commit.parents[0]]
  223. porcelain.diff_tree(
  224. r, parent_commit.tree, commit.tree, outstream=sys.stdout.buffer
  225. )
  226. class cmd_dump_pack(Command):
  227. def run(self, args) -> None:
  228. parser = argparse.ArgumentParser()
  229. parser.add_argument("filename", help="Pack file to dump")
  230. args = parser.parse_args(args)
  231. basename, _ = os.path.splitext(args.filename)
  232. x = Pack(basename)
  233. print(f"Object names checksum: {x.name()}")
  234. print(f"Checksum: {sha_to_hex(x.get_stored_checksum())}")
  235. x.check()
  236. print(f"Length: {len(x)}")
  237. for name in x:
  238. try:
  239. print(f"\t{x[name]}")
  240. except KeyError as k:
  241. print(f"\t{name}: Unable to resolve base {k}")
  242. except ApplyDeltaError as e:
  243. print(f"\t{name}: Unable to apply delta: {e!r}")
  244. class cmd_dump_index(Command):
  245. def run(self, args) -> None:
  246. parser = argparse.ArgumentParser()
  247. parser.add_argument("filename", help="Index file to dump")
  248. args = parser.parse_args(args)
  249. idx = Index(args.filename)
  250. for o in idx:
  251. print(o, idx[o])
  252. class cmd_init(Command):
  253. def run(self, args) -> None:
  254. parser = argparse.ArgumentParser()
  255. parser.add_argument(
  256. "--bare", action="store_true", help="Create a bare repository"
  257. )
  258. parser.add_argument(
  259. "path", nargs="?", default=os.getcwd(), help="Repository path"
  260. )
  261. args = parser.parse_args(args)
  262. porcelain.init(args.path, bare=args.bare)
  263. class cmd_clone(Command):
  264. def run(self, args) -> None:
  265. parser = argparse.ArgumentParser()
  266. parser.add_argument(
  267. "--bare",
  268. help="Whether to create a bare repository.",
  269. action="store_true",
  270. )
  271. parser.add_argument("--depth", type=int, help="Depth at which to fetch")
  272. parser.add_argument(
  273. "-b",
  274. "--branch",
  275. type=str,
  276. help="Check out branch instead of branch pointed to by remote HEAD",
  277. )
  278. parser.add_argument(
  279. "--refspec",
  280. type=str,
  281. help="References to fetch",
  282. action="append",
  283. )
  284. parser.add_argument(
  285. "--filter",
  286. dest="filter_spec",
  287. type=str,
  288. help="git-rev-list-style object filter",
  289. )
  290. parser.add_argument(
  291. "--protocol",
  292. type=int,
  293. help="Git protocol version to use",
  294. )
  295. parser.add_argument("source", help="Repository to clone from")
  296. parser.add_argument("target", nargs="?", help="Directory to clone into")
  297. args = parser.parse_args(args)
  298. try:
  299. porcelain.clone(
  300. args.source,
  301. args.target,
  302. bare=args.bare,
  303. depth=args.depth,
  304. branch=args.branch,
  305. refspec=args.refspec,
  306. filter_spec=args.filter_spec,
  307. protocol_version=args.protocol,
  308. )
  309. except GitProtocolError as e:
  310. print(f"{e}")
  311. class cmd_commit(Command):
  312. def run(self, args) -> None:
  313. parser = argparse.ArgumentParser()
  314. parser.add_argument("--message", "-m", required=True, help="Commit message")
  315. args = parser.parse_args(args)
  316. porcelain.commit(".", message=args.message)
  317. class cmd_commit_tree(Command):
  318. def run(self, args) -> None:
  319. parser = argparse.ArgumentParser()
  320. parser.add_argument("--message", "-m", required=True, help="Commit message")
  321. parser.add_argument("tree", help="Tree SHA to commit")
  322. args = parser.parse_args(args)
  323. porcelain.commit_tree(".", tree=args.tree, message=args.message)
  324. class cmd_update_server_info(Command):
  325. def run(self, args) -> None:
  326. porcelain.update_server_info(".")
  327. class cmd_symbolic_ref(Command):
  328. def run(self, args) -> None:
  329. parser = argparse.ArgumentParser()
  330. parser.add_argument("name", help="Symbolic reference name")
  331. parser.add_argument("ref", nargs="?", help="Target reference")
  332. parser.add_argument("--force", action="store_true", help="Force update")
  333. args = parser.parse_args(args)
  334. # If ref is provided, we're setting; otherwise we're reading
  335. if args.ref:
  336. # Set symbolic reference
  337. from .repo import Repo
  338. with Repo(".") as repo:
  339. repo.refs.set_symbolic_ref(args.name.encode(), args.ref.encode())
  340. else:
  341. # Read symbolic reference
  342. from .repo import Repo
  343. with Repo(".") as repo:
  344. try:
  345. target = repo.refs.read_ref(args.name.encode())
  346. if target.startswith(b"ref: "):
  347. print(target[5:].decode())
  348. else:
  349. print(target.decode())
  350. except KeyError:
  351. print(f"fatal: ref '{args.name}' is not a symbolic ref")
  352. class cmd_pack_refs(Command):
  353. def run(self, argv) -> None:
  354. parser = argparse.ArgumentParser()
  355. parser.add_argument("--all", action="store_true")
  356. # ignored, we never prune
  357. parser.add_argument("--no-prune", action="store_true")
  358. args = parser.parse_args(argv)
  359. porcelain.pack_refs(".", all=args.all)
  360. class cmd_show(Command):
  361. def run(self, argv) -> None:
  362. parser = argparse.ArgumentParser()
  363. parser.add_argument("objectish", type=str, nargs="*")
  364. args = parser.parse_args(argv)
  365. porcelain.show(".", args.objectish or None, outstream=sys.stdout)
  366. class cmd_diff_tree(Command):
  367. def run(self, args) -> None:
  368. parser = argparse.ArgumentParser()
  369. parser.add_argument("old_tree", help="Old tree SHA")
  370. parser.add_argument("new_tree", help="New tree SHA")
  371. args = parser.parse_args(args)
  372. porcelain.diff_tree(".", args.old_tree, args.new_tree)
  373. class cmd_rev_list(Command):
  374. def run(self, args) -> None:
  375. parser = argparse.ArgumentParser()
  376. parser.add_argument("commits", nargs="+", help="Commit IDs to list")
  377. args = parser.parse_args(args)
  378. porcelain.rev_list(".", args.commits)
  379. class cmd_tag(Command):
  380. def run(self, args) -> None:
  381. parser = argparse.ArgumentParser()
  382. parser.add_argument(
  383. "-a",
  384. "--annotated",
  385. help="Create an annotated tag.",
  386. action="store_true",
  387. )
  388. parser.add_argument(
  389. "-s", "--sign", help="Sign the annotated tag.", action="store_true"
  390. )
  391. parser.add_argument("tag_name", help="Name of the tag to create")
  392. args = parser.parse_args(args)
  393. porcelain.tag_create(
  394. ".", args.tag_name, annotated=args.annotated, sign=args.sign
  395. )
  396. class cmd_repack(Command):
  397. def run(self, args) -> None:
  398. parser = argparse.ArgumentParser()
  399. parser.parse_args(args)
  400. porcelain.repack(".")
  401. class cmd_reset(Command):
  402. def run(self, args) -> None:
  403. parser = argparse.ArgumentParser()
  404. mode_group = parser.add_mutually_exclusive_group()
  405. mode_group.add_argument(
  406. "--hard", action="store_true", help="Reset working tree and index"
  407. )
  408. mode_group.add_argument("--soft", action="store_true", help="Reset only HEAD")
  409. mode_group.add_argument(
  410. "--mixed", action="store_true", help="Reset HEAD and index"
  411. )
  412. parser.add_argument("treeish", nargs="?", help="Commit/tree to reset to")
  413. args = parser.parse_args(args)
  414. if args.hard:
  415. porcelain.reset(".", mode="hard", treeish=args.treeish)
  416. elif args.soft:
  417. # Soft reset: only change HEAD
  418. if args.treeish:
  419. from .repo import Repo
  420. with Repo(".") as repo:
  421. repo.refs[b"HEAD"] = args.treeish.encode()
  422. elif args.mixed:
  423. # Mixed reset is not implemented yet
  424. raise NotImplementedError("Mixed reset not yet implemented")
  425. else:
  426. # Default to mixed behavior (not implemented)
  427. raise NotImplementedError("Mixed reset not yet implemented")
  428. class cmd_daemon(Command):
  429. def run(self, args) -> None:
  430. from dulwich import log_utils
  431. from .protocol import TCP_GIT_PORT
  432. parser = argparse.ArgumentParser()
  433. parser.add_argument(
  434. "-l",
  435. "--listen_address",
  436. default="localhost",
  437. help="Binding IP address.",
  438. )
  439. parser.add_argument(
  440. "-p",
  441. "--port",
  442. type=int,
  443. default=TCP_GIT_PORT,
  444. help="Binding TCP port.",
  445. )
  446. parser.add_argument(
  447. "gitdir", nargs="?", default=".", help="Git directory to serve"
  448. )
  449. args = parser.parse_args(args)
  450. log_utils.default_logging_config()
  451. porcelain.daemon(args.gitdir, address=args.listen_address, port=args.port)
  452. class cmd_web_daemon(Command):
  453. def run(self, args) -> None:
  454. from dulwich import log_utils
  455. parser = argparse.ArgumentParser()
  456. parser.add_argument(
  457. "-l",
  458. "--listen_address",
  459. default="",
  460. help="Binding IP address.",
  461. )
  462. parser.add_argument(
  463. "-p",
  464. "--port",
  465. type=int,
  466. default=8000,
  467. help="Binding TCP port.",
  468. )
  469. parser.add_argument(
  470. "gitdir", nargs="?", default=".", help="Git directory to serve"
  471. )
  472. args = parser.parse_args(args)
  473. log_utils.default_logging_config()
  474. porcelain.web_daemon(args.gitdir, address=args.listen_address, port=args.port)
  475. class cmd_write_tree(Command):
  476. def run(self, args) -> None:
  477. parser = argparse.ArgumentParser()
  478. parser.parse_args(args)
  479. sys.stdout.write("{}\n".format(porcelain.write_tree(".").decode()))
  480. class cmd_receive_pack(Command):
  481. def run(self, args) -> None:
  482. parser = argparse.ArgumentParser()
  483. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  484. args = parser.parse_args(args)
  485. porcelain.receive_pack(args.gitdir)
  486. class cmd_upload_pack(Command):
  487. def run(self, args) -> None:
  488. parser = argparse.ArgumentParser()
  489. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  490. args = parser.parse_args(args)
  491. porcelain.upload_pack(args.gitdir)
  492. class cmd_status(Command):
  493. def run(self, args) -> None:
  494. parser = argparse.ArgumentParser()
  495. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  496. args = parser.parse_args(args)
  497. status = porcelain.status(args.gitdir)
  498. if any(names for (kind, names) in status.staged.items()):
  499. sys.stdout.write("Changes to be committed:\n\n")
  500. for kind, names in status.staged.items():
  501. for name in names:
  502. sys.stdout.write(
  503. f"\t{kind}: {name.decode(sys.getfilesystemencoding())}\n"
  504. )
  505. sys.stdout.write("\n")
  506. if status.unstaged:
  507. sys.stdout.write("Changes not staged for commit:\n\n")
  508. for name in status.unstaged:
  509. sys.stdout.write(f"\t{name.decode(sys.getfilesystemencoding())}\n")
  510. sys.stdout.write("\n")
  511. if status.untracked:
  512. sys.stdout.write("Untracked files:\n\n")
  513. for name in status.untracked:
  514. sys.stdout.write(f"\t{name}\n")
  515. sys.stdout.write("\n")
  516. class cmd_ls_remote(Command):
  517. def run(self, args) -> None:
  518. parser = argparse.ArgumentParser()
  519. parser.add_argument("url", help="Remote URL to list references from")
  520. args = parser.parse_args(args)
  521. refs = porcelain.ls_remote(args.url)
  522. for ref in sorted(refs):
  523. sys.stdout.write(f"{ref}\t{refs[ref]}\n")
  524. class cmd_ls_tree(Command):
  525. def run(self, args) -> None:
  526. parser = argparse.ArgumentParser()
  527. parser.add_argument(
  528. "-r",
  529. "--recursive",
  530. action="store_true",
  531. help="Recursively list tree contents.",
  532. )
  533. parser.add_argument(
  534. "--name-only", action="store_true", help="Only display name."
  535. )
  536. parser.add_argument("treeish", nargs="?", help="Tree-ish to list")
  537. args = parser.parse_args(args)
  538. porcelain.ls_tree(
  539. ".",
  540. args.treeish,
  541. outstream=sys.stdout,
  542. recursive=args.recursive,
  543. name_only=args.name_only,
  544. )
  545. class cmd_pack_objects(Command):
  546. def run(self, args) -> None:
  547. parser = argparse.ArgumentParser()
  548. parser.add_argument(
  549. "--stdout", action="store_true", help="Write pack to stdout"
  550. )
  551. parser.add_argument("--deltify", action="store_true", help="Create deltas")
  552. parser.add_argument(
  553. "--no-reuse-deltas", action="store_true", help="Don't reuse existing deltas"
  554. )
  555. parser.add_argument("basename", nargs="?", help="Base name for pack files")
  556. args = parser.parse_args(args)
  557. if not args.stdout and not args.basename:
  558. parser.error("basename required when not using --stdout")
  559. object_ids = [line.strip() for line in sys.stdin.readlines()]
  560. deltify = args.deltify
  561. reuse_deltas = not args.no_reuse_deltas
  562. if args.stdout:
  563. packf = getattr(sys.stdout, "buffer", sys.stdout)
  564. idxf = None
  565. close = []
  566. else:
  567. packf = open(args.basename + ".pack", "wb")
  568. idxf = open(args.basename + ".idx", "wb")
  569. close = [packf, idxf]
  570. porcelain.pack_objects(
  571. ".", object_ids, packf, idxf, deltify=deltify, reuse_deltas=reuse_deltas
  572. )
  573. for f in close:
  574. f.close()
  575. class cmd_unpack_objects(Command):
  576. def run(self, args) -> None:
  577. parser = argparse.ArgumentParser()
  578. parser.add_argument("pack_file", help="Pack file to unpack")
  579. args = parser.parse_args(args)
  580. count = porcelain.unpack_objects(args.pack_file)
  581. print(f"Unpacked {count} objects")
  582. class cmd_pull(Command):
  583. def run(self, args) -> None:
  584. parser = argparse.ArgumentParser()
  585. parser.add_argument("from_location", type=str)
  586. parser.add_argument("refspec", type=str, nargs="*")
  587. parser.add_argument("--filter", type=str, nargs=1)
  588. parser.add_argument("--protocol", type=int)
  589. args = parser.parse_args(args)
  590. porcelain.pull(
  591. ".",
  592. args.from_location or None,
  593. args.refspec or None,
  594. filter_spec=args.filter,
  595. protocol_version=args.protocol or None,
  596. )
  597. class cmd_push(Command):
  598. def run(self, argv) -> Optional[int]:
  599. parser = argparse.ArgumentParser()
  600. parser.add_argument("-f", "--force", action="store_true", help="Force")
  601. parser.add_argument("to_location", type=str)
  602. parser.add_argument("refspec", type=str, nargs="*")
  603. args = parser.parse_args(argv)
  604. try:
  605. porcelain.push(
  606. ".", args.to_location, args.refspec or None, force=args.force
  607. )
  608. except porcelain.DivergedBranches:
  609. sys.stderr.write("Diverged branches; specify --force to override")
  610. return 1
  611. return None
  612. class cmd_remote_add(Command):
  613. def run(self, args) -> None:
  614. parser = argparse.ArgumentParser()
  615. parser.add_argument("name", help="Name of the remote")
  616. parser.add_argument("url", help="URL of the remote")
  617. args = parser.parse_args(args)
  618. porcelain.remote_add(".", args.name, args.url)
  619. class SuperCommand(Command):
  620. subcommands: ClassVar[dict[str, type[Command]]] = {}
  621. default_command: ClassVar[Optional[type[Command]]] = None
  622. def run(self, args):
  623. if not args:
  624. if self.default_command:
  625. return self.default_command().run(args)
  626. else:
  627. print(
  628. "Supported subcommands: {}".format(
  629. ", ".join(self.subcommands.keys())
  630. )
  631. )
  632. return False
  633. cmd = args[0]
  634. try:
  635. cmd_kls = self.subcommands[cmd]
  636. except KeyError:
  637. print(f"No such subcommand: {args[0]}")
  638. return False
  639. return cmd_kls().run(args[1:])
  640. class cmd_remote(SuperCommand):
  641. subcommands: ClassVar[dict[str, type[Command]]] = {
  642. "add": cmd_remote_add,
  643. }
  644. class cmd_submodule_list(Command):
  645. def run(self, argv) -> None:
  646. parser = argparse.ArgumentParser()
  647. parser.parse_args(argv)
  648. for path, sha in porcelain.submodule_list("."):
  649. sys.stdout.write(f" {sha} {path}\n")
  650. class cmd_submodule_init(Command):
  651. def run(self, argv) -> None:
  652. parser = argparse.ArgumentParser()
  653. parser.parse_args(argv)
  654. porcelain.submodule_init(".")
  655. class cmd_submodule(SuperCommand):
  656. subcommands: ClassVar[dict[str, type[Command]]] = {
  657. "init": cmd_submodule_init,
  658. "list": cmd_submodule_list,
  659. }
  660. default_command = cmd_submodule_list
  661. class cmd_check_ignore(Command):
  662. def run(self, args):
  663. parser = argparse.ArgumentParser()
  664. parser.add_argument("paths", nargs="+", help="Paths to check")
  665. args = parser.parse_args(args)
  666. ret = 1
  667. for path in porcelain.check_ignore(".", args.paths):
  668. print(path)
  669. ret = 0
  670. return ret
  671. class cmd_check_mailmap(Command):
  672. def run(self, args) -> None:
  673. parser = argparse.ArgumentParser()
  674. parser.add_argument("identities", nargs="+", help="Identities to check")
  675. args = parser.parse_args(args)
  676. for identity in args.identities:
  677. canonical_identity = porcelain.check_mailmap(".", identity)
  678. print(canonical_identity)
  679. class cmd_branch(Command):
  680. def run(self, args) -> None:
  681. parser = argparse.ArgumentParser()
  682. parser.add_argument(
  683. "branch",
  684. type=str,
  685. help="Name of the branch",
  686. )
  687. parser.add_argument(
  688. "-d",
  689. "--delete",
  690. action="store_true",
  691. help="Delete branch",
  692. )
  693. args = parser.parse_args(args)
  694. if not args.branch:
  695. print("Usage: dulwich branch [-d] BRANCH_NAME")
  696. sys.exit(1)
  697. if args.delete:
  698. porcelain.branch_delete(".", name=args.branch)
  699. else:
  700. try:
  701. porcelain.branch_create(".", name=args.branch)
  702. except porcelain.Error as e:
  703. sys.stderr.write(f"{e}")
  704. sys.exit(1)
  705. class cmd_checkout(Command):
  706. def run(self, args) -> None:
  707. parser = argparse.ArgumentParser()
  708. parser.add_argument(
  709. "target",
  710. type=str,
  711. help="Name of the branch, tag, or commit to checkout",
  712. )
  713. parser.add_argument(
  714. "-f",
  715. "--force",
  716. action="store_true",
  717. help="Force checkout",
  718. )
  719. parser.add_argument(
  720. "-b",
  721. "--new-branch",
  722. type=str,
  723. help="Create a new branch at the target and switch to it",
  724. )
  725. args = parser.parse_args(args)
  726. if not args.target:
  727. print("Usage: dulwich checkout TARGET [--force] [-b NEW_BRANCH]")
  728. sys.exit(1)
  729. try:
  730. porcelain.checkout(
  731. ".", target=args.target, force=args.force, new_branch=args.new_branch
  732. )
  733. except porcelain.CheckoutError as e:
  734. sys.stderr.write(f"{e}\n")
  735. sys.exit(1)
  736. class cmd_stash_list(Command):
  737. def run(self, args) -> None:
  738. parser = argparse.ArgumentParser()
  739. parser.parse_args(args)
  740. for i, entry in porcelain.stash_list("."):
  741. print("stash@{{{}}}: {}".format(i, entry.message.rstrip("\n")))
  742. class cmd_stash_push(Command):
  743. def run(self, args) -> None:
  744. parser = argparse.ArgumentParser()
  745. parser.parse_args(args)
  746. porcelain.stash_push(".")
  747. print("Saved working directory and index state")
  748. class cmd_stash_pop(Command):
  749. def run(self, args) -> None:
  750. parser = argparse.ArgumentParser()
  751. parser.parse_args(args)
  752. porcelain.stash_pop(".")
  753. print("Restored working directory and index state")
  754. class cmd_stash(SuperCommand):
  755. subcommands: ClassVar[dict[str, type[Command]]] = {
  756. "list": cmd_stash_list,
  757. "pop": cmd_stash_pop,
  758. "push": cmd_stash_push,
  759. }
  760. class cmd_ls_files(Command):
  761. def run(self, args) -> None:
  762. parser = argparse.ArgumentParser()
  763. parser.parse_args(args)
  764. for name in porcelain.ls_files("."):
  765. print(name)
  766. class cmd_describe(Command):
  767. def run(self, args) -> None:
  768. parser = argparse.ArgumentParser()
  769. parser.parse_args(args)
  770. print(porcelain.describe("."))
  771. class cmd_merge(Command):
  772. def run(self, args) -> Optional[int]:
  773. parser = argparse.ArgumentParser()
  774. parser.add_argument("commit", type=str, help="Commit to merge")
  775. parser.add_argument(
  776. "--no-commit", action="store_true", help="Do not create a merge commit"
  777. )
  778. parser.add_argument(
  779. "--no-ff", action="store_true", help="Force create a merge commit"
  780. )
  781. parser.add_argument("-m", "--message", type=str, help="Merge commit message")
  782. args = parser.parse_args(args)
  783. try:
  784. merge_commit_id, conflicts = porcelain.merge(
  785. ".",
  786. args.commit,
  787. no_commit=args.no_commit,
  788. no_ff=args.no_ff,
  789. message=args.message,
  790. )
  791. if conflicts:
  792. print(f"Merge conflicts in {len(conflicts)} file(s):")
  793. for conflict_path in conflicts:
  794. print(f" {conflict_path.decode()}")
  795. print(
  796. "\nAutomatic merge failed; fix conflicts and then commit the result."
  797. )
  798. return 1
  799. elif merge_commit_id is None and not args.no_commit:
  800. print("Already up to date.")
  801. elif args.no_commit:
  802. print("Automatic merge successful; not committing as requested.")
  803. else:
  804. print(
  805. f"Merge successful. Created merge commit {merge_commit_id.decode()}"
  806. )
  807. return None
  808. except porcelain.Error as e:
  809. print(f"Error: {e}")
  810. return 1
  811. class cmd_notes_add(Command):
  812. def run(self, args) -> None:
  813. parser = argparse.ArgumentParser()
  814. parser.add_argument("object", help="Object to annotate")
  815. parser.add_argument("-m", "--message", help="Note message", required=True)
  816. parser.add_argument(
  817. "--ref", default="commits", help="Notes ref (default: commits)"
  818. )
  819. args = parser.parse_args(args)
  820. porcelain.notes_add(".", args.object, args.message, ref=args.ref)
  821. class cmd_notes_show(Command):
  822. def run(self, args) -> None:
  823. parser = argparse.ArgumentParser()
  824. parser.add_argument("object", help="Object to show notes for")
  825. parser.add_argument(
  826. "--ref", default="commits", help="Notes ref (default: commits)"
  827. )
  828. args = parser.parse_args(args)
  829. note = porcelain.notes_show(".", args.object, ref=args.ref)
  830. if note:
  831. sys.stdout.buffer.write(note)
  832. else:
  833. print(f"No notes found for object {args.object}")
  834. class cmd_notes_remove(Command):
  835. def run(self, args) -> None:
  836. parser = argparse.ArgumentParser()
  837. parser.add_argument("object", help="Object to remove notes from")
  838. parser.add_argument(
  839. "--ref", default="commits", help="Notes ref (default: commits)"
  840. )
  841. args = parser.parse_args(args)
  842. result = porcelain.notes_remove(".", args.object, ref=args.ref)
  843. if result:
  844. print(f"Removed notes for object {args.object}")
  845. else:
  846. print(f"No notes found for object {args.object}")
  847. class cmd_notes_list(Command):
  848. def run(self, args) -> None:
  849. parser = argparse.ArgumentParser()
  850. parser.add_argument(
  851. "--ref", default="commits", help="Notes ref (default: commits)"
  852. )
  853. args = parser.parse_args(args)
  854. notes = porcelain.notes_list(".", ref=args.ref)
  855. for object_sha, note_content in notes:
  856. print(f"{object_sha.hex()}")
  857. class cmd_notes(SuperCommand):
  858. subcommands: ClassVar[dict[str, type[Command]]] = {
  859. "add": cmd_notes_add,
  860. "show": cmd_notes_show,
  861. "remove": cmd_notes_remove,
  862. "list": cmd_notes_list,
  863. }
  864. default_command = cmd_notes_list
  865. class cmd_merge_tree(Command):
  866. def run(self, args) -> Optional[int]:
  867. parser = argparse.ArgumentParser(
  868. description="Perform a tree-level merge without touching the working directory"
  869. )
  870. parser.add_argument(
  871. "base_tree",
  872. nargs="?",
  873. help="The common ancestor tree (optional, defaults to empty tree)",
  874. )
  875. parser.add_argument("our_tree", help="Our side of the merge")
  876. parser.add_argument("their_tree", help="Their side of the merge")
  877. parser.add_argument(
  878. "-z",
  879. "--name-only",
  880. action="store_true",
  881. help="Output only conflict paths, null-terminated",
  882. )
  883. args = parser.parse_args(args)
  884. try:
  885. # Determine base tree - if only two args provided, base is None
  886. if args.base_tree is None:
  887. # Only two arguments provided
  888. base_tree = None
  889. our_tree = args.our_tree
  890. their_tree = args.their_tree
  891. else:
  892. # Three arguments provided
  893. base_tree = args.base_tree
  894. our_tree = args.our_tree
  895. their_tree = args.their_tree
  896. merged_tree_id, conflicts = porcelain.merge_tree(
  897. ".", base_tree, our_tree, their_tree
  898. )
  899. if args.name_only:
  900. # Output only conflict paths, null-terminated
  901. for conflict_path in conflicts:
  902. sys.stdout.buffer.write(conflict_path)
  903. sys.stdout.buffer.write(b"\0")
  904. else:
  905. # Output the merged tree SHA
  906. print(merged_tree_id.decode("ascii"))
  907. # Output conflict information
  908. if conflicts:
  909. print(f"\nConflicts in {len(conflicts)} file(s):")
  910. for conflict_path in conflicts:
  911. print(f" {conflict_path.decode()}")
  912. return None
  913. except porcelain.Error as e:
  914. print(f"Error: {e}", file=sys.stderr)
  915. return 1
  916. except KeyError as e:
  917. print(f"Error: Object not found: {e}", file=sys.stderr)
  918. return 1
  919. class cmd_gc(Command):
  920. def run(self, args) -> Optional[int]:
  921. import datetime
  922. import time
  923. parser = argparse.ArgumentParser()
  924. parser.add_argument(
  925. "--auto",
  926. action="store_true",
  927. help="Only run gc if needed",
  928. )
  929. parser.add_argument(
  930. "--aggressive",
  931. action="store_true",
  932. help="Use more aggressive settings",
  933. )
  934. parser.add_argument(
  935. "--no-prune",
  936. action="store_true",
  937. help="Do not prune unreachable objects",
  938. )
  939. parser.add_argument(
  940. "--prune",
  941. nargs="?",
  942. const="now",
  943. help="Prune unreachable objects older than date (default: 2 weeks ago)",
  944. )
  945. parser.add_argument(
  946. "--dry-run",
  947. "-n",
  948. action="store_true",
  949. help="Only report what would be done",
  950. )
  951. parser.add_argument(
  952. "--quiet",
  953. "-q",
  954. action="store_true",
  955. help="Only report errors",
  956. )
  957. args = parser.parse_args(args)
  958. # Parse prune grace period
  959. grace_period = None
  960. if args.prune:
  961. try:
  962. grace_period = parse_relative_time(args.prune)
  963. except ValueError:
  964. # Try to parse as absolute date
  965. try:
  966. date = datetime.datetime.strptime(args.prune, "%Y-%m-%d")
  967. grace_period = int(time.time() - date.timestamp())
  968. except ValueError:
  969. print(f"Error: Invalid prune date: {args.prune}")
  970. return 1
  971. elif not args.no_prune:
  972. # Default to 2 weeks
  973. grace_period = 1209600
  974. # Progress callback
  975. def progress(msg):
  976. if not args.quiet:
  977. print(msg)
  978. try:
  979. stats = porcelain.gc(
  980. ".",
  981. auto=args.auto,
  982. aggressive=args.aggressive,
  983. prune=not args.no_prune,
  984. grace_period=grace_period,
  985. dry_run=args.dry_run,
  986. progress=progress if not args.quiet else None,
  987. )
  988. # Report results
  989. if not args.quiet:
  990. if args.dry_run:
  991. print("\nDry run results:")
  992. else:
  993. print("\nGarbage collection complete:")
  994. if stats.pruned_objects:
  995. print(f" Pruned {len(stats.pruned_objects)} unreachable objects")
  996. print(f" Freed {format_bytes(stats.bytes_freed)}")
  997. if stats.packs_before != stats.packs_after:
  998. print(
  999. f" Reduced pack files from {stats.packs_before} to {stats.packs_after}"
  1000. )
  1001. except porcelain.Error as e:
  1002. print(f"Error: {e}")
  1003. return 1
  1004. return None
  1005. class cmd_count_objects(Command):
  1006. def run(self, args) -> None:
  1007. parser = argparse.ArgumentParser()
  1008. parser.add_argument(
  1009. "-v",
  1010. "--verbose",
  1011. action="store_true",
  1012. help="Display verbose information.",
  1013. )
  1014. args = parser.parse_args(args)
  1015. if args.verbose:
  1016. stats = porcelain.count_objects(".", verbose=True)
  1017. # Display verbose output
  1018. print(f"count: {stats.count}")
  1019. print(f"size: {stats.size // 1024}") # Size in KiB
  1020. assert stats.in_pack is not None
  1021. print(f"in-pack: {stats.in_pack}")
  1022. assert stats.packs is not None
  1023. print(f"packs: {stats.packs}")
  1024. assert stats.size_pack is not None
  1025. print(f"size-pack: {stats.size_pack // 1024}") # Size in KiB
  1026. else:
  1027. # Simple output
  1028. stats = porcelain.count_objects(".", verbose=False)
  1029. print(f"{stats.count} objects, {stats.size // 1024} kilobytes")
  1030. class cmd_rebase(Command):
  1031. def run(self, args) -> int:
  1032. parser = argparse.ArgumentParser()
  1033. parser.add_argument(
  1034. "upstream", nargs="?", help="Upstream branch to rebase onto"
  1035. )
  1036. parser.add_argument("--onto", type=str, help="Rebase onto specific commit")
  1037. parser.add_argument(
  1038. "--branch", type=str, help="Branch to rebase (default: current)"
  1039. )
  1040. parser.add_argument(
  1041. "--abort", action="store_true", help="Abort an in-progress rebase"
  1042. )
  1043. parser.add_argument(
  1044. "--continue",
  1045. dest="continue_rebase",
  1046. action="store_true",
  1047. help="Continue an in-progress rebase",
  1048. )
  1049. parser.add_argument(
  1050. "--skip", action="store_true", help="Skip current commit and continue"
  1051. )
  1052. args = parser.parse_args(args)
  1053. # Handle abort/continue/skip first
  1054. if args.abort:
  1055. try:
  1056. porcelain.rebase(".", args.upstream or "HEAD", abort=True)
  1057. print("Rebase aborted.")
  1058. except porcelain.Error as e:
  1059. print(f"Error: {e}")
  1060. return 1
  1061. return 0
  1062. if args.continue_rebase:
  1063. try:
  1064. new_shas = porcelain.rebase(
  1065. ".", args.upstream or "HEAD", continue_rebase=True
  1066. )
  1067. print("Rebase complete.")
  1068. except porcelain.Error as e:
  1069. print(f"Error: {e}")
  1070. return 1
  1071. return 0
  1072. # Normal rebase requires upstream
  1073. if not args.upstream:
  1074. print("Error: Missing required argument 'upstream'")
  1075. return 1
  1076. try:
  1077. new_shas = porcelain.rebase(
  1078. ".",
  1079. args.upstream,
  1080. onto=args.onto,
  1081. branch=args.branch,
  1082. )
  1083. if new_shas:
  1084. print(f"Successfully rebased {len(new_shas)} commits.")
  1085. else:
  1086. print("Already up to date.")
  1087. return 0
  1088. except porcelain.Error as e:
  1089. print(f"Error: {e}")
  1090. return 1
  1091. class cmd_help(Command):
  1092. def run(self, args) -> None:
  1093. parser = argparse.ArgumentParser()
  1094. parser.add_argument(
  1095. "-a",
  1096. "--all",
  1097. action="store_true",
  1098. help="List all commands.",
  1099. )
  1100. args = parser.parse_args(args)
  1101. if args.all:
  1102. print("Available commands:")
  1103. for cmd in sorted(commands):
  1104. print(f" {cmd}")
  1105. else:
  1106. print(
  1107. """\
  1108. The dulwich command line tool is currently a very basic frontend for the
  1109. Dulwich python module. For full functionality, please see the API reference.
  1110. For a list of supported commands, see 'dulwich help -a'.
  1111. """
  1112. )
  1113. commands = {
  1114. "add": cmd_add,
  1115. "archive": cmd_archive,
  1116. "branch": cmd_branch,
  1117. "check-ignore": cmd_check_ignore,
  1118. "check-mailmap": cmd_check_mailmap,
  1119. "checkout": cmd_checkout,
  1120. "clone": cmd_clone,
  1121. "commit": cmd_commit,
  1122. "commit-tree": cmd_commit_tree,
  1123. "count-objects": cmd_count_objects,
  1124. "describe": cmd_describe,
  1125. "daemon": cmd_daemon,
  1126. "diff": cmd_diff,
  1127. "diff-tree": cmd_diff_tree,
  1128. "dump-pack": cmd_dump_pack,
  1129. "dump-index": cmd_dump_index,
  1130. "fetch-pack": cmd_fetch_pack,
  1131. "fetch": cmd_fetch,
  1132. "for-each-ref": cmd_for_each_ref,
  1133. "fsck": cmd_fsck,
  1134. "gc": cmd_gc,
  1135. "help": cmd_help,
  1136. "init": cmd_init,
  1137. "log": cmd_log,
  1138. "ls-files": cmd_ls_files,
  1139. "ls-remote": cmd_ls_remote,
  1140. "ls-tree": cmd_ls_tree,
  1141. "merge": cmd_merge,
  1142. "merge-tree": cmd_merge_tree,
  1143. "notes": cmd_notes,
  1144. "pack-objects": cmd_pack_objects,
  1145. "pack-refs": cmd_pack_refs,
  1146. "pull": cmd_pull,
  1147. "push": cmd_push,
  1148. "rebase": cmd_rebase,
  1149. "receive-pack": cmd_receive_pack,
  1150. "remote": cmd_remote,
  1151. "repack": cmd_repack,
  1152. "reset": cmd_reset,
  1153. "rev-list": cmd_rev_list,
  1154. "rm": cmd_rm,
  1155. "show": cmd_show,
  1156. "stash": cmd_stash,
  1157. "status": cmd_status,
  1158. "symbolic-ref": cmd_symbolic_ref,
  1159. "submodule": cmd_submodule,
  1160. "tag": cmd_tag,
  1161. "unpack-objects": cmd_unpack_objects,
  1162. "update-server-info": cmd_update_server_info,
  1163. "upload-pack": cmd_upload_pack,
  1164. "web-daemon": cmd_web_daemon,
  1165. "write-tree": cmd_write_tree,
  1166. }
  1167. def main(argv=None):
  1168. if argv is None:
  1169. argv = sys.argv[1:]
  1170. if len(argv) < 1:
  1171. print("Usage: dulwich <{}> [OPTIONS...]".format("|".join(commands.keys())))
  1172. return 1
  1173. cmd = argv[0]
  1174. try:
  1175. cmd_kls = commands[cmd]
  1176. except KeyError:
  1177. print(f"No such subcommand: {cmd}")
  1178. return 1
  1179. # TODO(jelmer): Return non-0 on errors
  1180. return cmd_kls().run(argv[1:])
  1181. def _main() -> None:
  1182. if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
  1183. signal.signal(signal.SIGQUIT, signal_quit) # type: ignore
  1184. signal.signal(signal.SIGINT, signal_int)
  1185. sys.exit(main())
  1186. if __name__ == "__main__":
  1187. _main()