cli.py 48 KB

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