cli.py 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  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. class Command:
  49. """A Dulwich subcommand."""
  50. def run(self, args) -> Optional[int]:
  51. """Run the command."""
  52. raise NotImplementedError(self.run)
  53. class cmd_archive(Command):
  54. def run(self, args) -> None:
  55. parser = argparse.ArgumentParser()
  56. parser.add_argument(
  57. "--remote",
  58. type=str,
  59. help="Retrieve archive from specified remote repo",
  60. )
  61. parser.add_argument("committish", type=str, nargs="?")
  62. args = parser.parse_args(args)
  63. if args.remote:
  64. client, path = get_transport_and_path(args.remote)
  65. client.archive(
  66. path,
  67. args.committish,
  68. sys.stdout.write,
  69. write_error=sys.stderr.write,
  70. )
  71. else:
  72. # Use buffer if available (for binary output), otherwise use stdout
  73. outstream = getattr(sys.stdout, "buffer", sys.stdout)
  74. porcelain.archive(
  75. ".", args.committish, outstream=outstream, errstream=sys.stderr
  76. )
  77. class cmd_add(Command):
  78. def run(self, argv) -> None:
  79. parser = argparse.ArgumentParser()
  80. parser.add_argument("path", nargs="+")
  81. args = parser.parse_args(argv)
  82. # Convert '.' to None to add all files
  83. paths = args.path
  84. if len(paths) == 1 and paths[0] == ".":
  85. paths = None
  86. porcelain.add(".", paths=paths)
  87. class cmd_rm(Command):
  88. def run(self, argv) -> None:
  89. parser = argparse.ArgumentParser()
  90. parser.add_argument(
  91. "--cached", action="store_true", help="Remove from index only"
  92. )
  93. parser.add_argument("path", type=Path, nargs="+")
  94. args = parser.parse_args(argv)
  95. porcelain.remove(".", paths=args.path, cached=args.cached)
  96. class cmd_fetch_pack(Command):
  97. def run(self, argv) -> None:
  98. parser = argparse.ArgumentParser()
  99. parser.add_argument("--all", action="store_true")
  100. parser.add_argument("location", nargs="?", type=str)
  101. parser.add_argument("refs", nargs="*", type=str)
  102. args = parser.parse_args(argv)
  103. client, path = get_transport_and_path(args.location)
  104. r = Repo(".")
  105. if args.all:
  106. determine_wants = r.object_store.determine_wants_all
  107. else:
  108. def determine_wants(refs, depth: Optional[int] = None):
  109. return [y.encode("utf-8") for y in args.refs if y not in r.object_store]
  110. client.fetch(path, r, determine_wants)
  111. class cmd_fetch(Command):
  112. def run(self, args) -> None:
  113. parser = argparse.ArgumentParser()
  114. parser.add_argument("location", help="Remote location to fetch from")
  115. args = parser.parse_args(args)
  116. client, path = get_transport_and_path(args.location)
  117. r = Repo(".")
  118. def progress(msg: bytes) -> None:
  119. sys.stdout.buffer.write(msg)
  120. refs = client.fetch(path, r, progress=progress)
  121. print("Remote refs:")
  122. for item in refs.items():
  123. print("{} -> {}".format(*item))
  124. class cmd_for_each_ref(Command):
  125. def run(self, args) -> None:
  126. parser = argparse.ArgumentParser()
  127. parser.add_argument("pattern", type=str, nargs="?")
  128. args = parser.parse_args(args)
  129. for sha, object_type, ref in porcelain.for_each_ref(".", args.pattern):
  130. print(f"{sha.decode()} {object_type.decode()}\t{ref.decode()}")
  131. class cmd_fsck(Command):
  132. def run(self, args) -> None:
  133. parser = argparse.ArgumentParser()
  134. parser.parse_args(args)
  135. for obj, msg in porcelain.fsck("."):
  136. print(f"{obj}: {msg}")
  137. class cmd_log(Command):
  138. def run(self, args) -> None:
  139. parser = argparse.ArgumentParser()
  140. parser.add_argument(
  141. "--reverse",
  142. action="store_true",
  143. help="Reverse order in which entries are printed",
  144. )
  145. parser.add_argument(
  146. "--name-status",
  147. action="store_true",
  148. help="Print name/status for each changed file",
  149. )
  150. parser.add_argument("paths", nargs="*", help="Paths to show log for")
  151. args = parser.parse_args(args)
  152. porcelain.log(
  153. ".",
  154. paths=args.paths,
  155. reverse=args.reverse,
  156. name_status=args.name_status,
  157. outstream=sys.stdout,
  158. )
  159. class cmd_diff(Command):
  160. def run(self, args) -> None:
  161. parser = argparse.ArgumentParser()
  162. parser.add_argument(
  163. "commit", nargs="?", default="HEAD", help="Commit to show diff for"
  164. )
  165. args = parser.parse_args(args)
  166. r = Repo(".")
  167. commit_id = (
  168. args.commit.encode() if isinstance(args.commit, str) else args.commit
  169. )
  170. commit = parse_commit(r, commit_id)
  171. parent_commit = r[commit.parents[0]]
  172. porcelain.diff_tree(
  173. r, parent_commit.tree, commit.tree, outstream=sys.stdout.buffer
  174. )
  175. class cmd_dump_pack(Command):
  176. def run(self, args) -> None:
  177. parser = argparse.ArgumentParser()
  178. parser.add_argument("filename", help="Pack file to dump")
  179. args = parser.parse_args(args)
  180. basename, _ = os.path.splitext(args.filename)
  181. x = Pack(basename)
  182. print(f"Object names checksum: {x.name()}")
  183. print(f"Checksum: {sha_to_hex(x.get_stored_checksum())}")
  184. x.check()
  185. print(f"Length: {len(x)}")
  186. for name in x:
  187. try:
  188. print(f"\t{x[name]}")
  189. except KeyError as k:
  190. print(f"\t{name}: Unable to resolve base {k}")
  191. except ApplyDeltaError as e:
  192. print(f"\t{name}: Unable to apply delta: {e!r}")
  193. class cmd_dump_index(Command):
  194. def run(self, args) -> None:
  195. parser = argparse.ArgumentParser()
  196. parser.add_argument("filename", help="Index file to dump")
  197. args = parser.parse_args(args)
  198. idx = Index(args.filename)
  199. for o in idx:
  200. print(o, idx[o])
  201. class cmd_init(Command):
  202. def run(self, args) -> None:
  203. parser = argparse.ArgumentParser()
  204. parser.add_argument(
  205. "--bare", action="store_true", help="Create a bare repository"
  206. )
  207. parser.add_argument(
  208. "path", nargs="?", default=os.getcwd(), help="Repository path"
  209. )
  210. args = parser.parse_args(args)
  211. porcelain.init(args.path, bare=args.bare)
  212. class cmd_clone(Command):
  213. def run(self, args) -> None:
  214. parser = argparse.ArgumentParser()
  215. parser.add_argument(
  216. "--bare",
  217. help="Whether to create a bare repository.",
  218. action="store_true",
  219. )
  220. parser.add_argument("--depth", type=int, help="Depth at which to fetch")
  221. parser.add_argument(
  222. "-b",
  223. "--branch",
  224. type=str,
  225. help="Check out branch instead of branch pointed to by remote HEAD",
  226. )
  227. parser.add_argument(
  228. "--refspec",
  229. type=str,
  230. help="References to fetch",
  231. action="append",
  232. )
  233. parser.add_argument(
  234. "--filter",
  235. dest="filter_spec",
  236. type=str,
  237. help="git-rev-list-style object filter",
  238. )
  239. parser.add_argument(
  240. "--protocol",
  241. type=int,
  242. help="Git protocol version to use",
  243. )
  244. parser.add_argument("source", help="Repository to clone from")
  245. parser.add_argument("target", nargs="?", help="Directory to clone into")
  246. args = parser.parse_args(args)
  247. try:
  248. porcelain.clone(
  249. args.source,
  250. args.target,
  251. bare=args.bare,
  252. depth=args.depth,
  253. branch=args.branch,
  254. refspec=args.refspec,
  255. filter_spec=args.filter_spec,
  256. protocol_version=args.protocol,
  257. )
  258. except GitProtocolError as e:
  259. print(f"{e}")
  260. class cmd_commit(Command):
  261. def run(self, args) -> None:
  262. parser = argparse.ArgumentParser()
  263. parser.add_argument("--message", "-m", required=True, help="Commit message")
  264. args = parser.parse_args(args)
  265. porcelain.commit(".", message=args.message)
  266. class cmd_commit_tree(Command):
  267. def run(self, args) -> None:
  268. parser = argparse.ArgumentParser()
  269. parser.add_argument("--message", "-m", required=True, help="Commit message")
  270. parser.add_argument("tree", help="Tree SHA to commit")
  271. args = parser.parse_args(args)
  272. porcelain.commit_tree(".", tree=args.tree, message=args.message)
  273. class cmd_update_server_info(Command):
  274. def run(self, args) -> None:
  275. porcelain.update_server_info(".")
  276. class cmd_symbolic_ref(Command):
  277. def run(self, args) -> None:
  278. parser = argparse.ArgumentParser()
  279. parser.add_argument("name", help="Symbolic reference name")
  280. parser.add_argument("ref", nargs="?", help="Target reference")
  281. parser.add_argument("--force", action="store_true", help="Force update")
  282. args = parser.parse_args(args)
  283. # If ref is provided, we're setting; otherwise we're reading
  284. if args.ref:
  285. # Set symbolic reference
  286. from .repo import Repo
  287. with Repo(".") as repo:
  288. repo.refs.set_symbolic_ref(args.name.encode(), args.ref.encode())
  289. else:
  290. # Read symbolic reference
  291. from .repo import Repo
  292. with Repo(".") as repo:
  293. try:
  294. target = repo.refs.read_ref(args.name.encode())
  295. if target.startswith(b"ref: "):
  296. print(target[5:].decode())
  297. else:
  298. print(target.decode())
  299. except KeyError:
  300. print(f"fatal: ref '{args.name}' is not a symbolic ref")
  301. class cmd_pack_refs(Command):
  302. def run(self, argv) -> None:
  303. parser = argparse.ArgumentParser()
  304. parser.add_argument("--all", action="store_true")
  305. # ignored, we never prune
  306. parser.add_argument("--no-prune", action="store_true")
  307. args = parser.parse_args(argv)
  308. porcelain.pack_refs(".", all=args.all)
  309. class cmd_show(Command):
  310. def run(self, argv) -> None:
  311. parser = argparse.ArgumentParser()
  312. parser.add_argument("objectish", type=str, nargs="*")
  313. args = parser.parse_args(argv)
  314. porcelain.show(".", args.objectish or None, outstream=sys.stdout)
  315. class cmd_diff_tree(Command):
  316. def run(self, args) -> None:
  317. parser = argparse.ArgumentParser()
  318. parser.add_argument("old_tree", help="Old tree SHA")
  319. parser.add_argument("new_tree", help="New tree SHA")
  320. args = parser.parse_args(args)
  321. porcelain.diff_tree(".", args.old_tree, args.new_tree)
  322. class cmd_rev_list(Command):
  323. def run(self, args) -> None:
  324. parser = argparse.ArgumentParser()
  325. parser.add_argument("commits", nargs="+", help="Commit IDs to list")
  326. args = parser.parse_args(args)
  327. porcelain.rev_list(".", args.commits)
  328. class cmd_tag(Command):
  329. def run(self, args) -> None:
  330. parser = argparse.ArgumentParser()
  331. parser.add_argument(
  332. "-a",
  333. "--annotated",
  334. help="Create an annotated tag.",
  335. action="store_true",
  336. )
  337. parser.add_argument(
  338. "-s", "--sign", help="Sign the annotated tag.", action="store_true"
  339. )
  340. parser.add_argument("tag_name", help="Name of the tag to create")
  341. args = parser.parse_args(args)
  342. porcelain.tag_create(
  343. ".", args.tag_name, annotated=args.annotated, sign=args.sign
  344. )
  345. class cmd_repack(Command):
  346. def run(self, args) -> None:
  347. parser = argparse.ArgumentParser()
  348. parser.parse_args(args)
  349. porcelain.repack(".")
  350. class cmd_reset(Command):
  351. def run(self, args) -> None:
  352. parser = argparse.ArgumentParser()
  353. mode_group = parser.add_mutually_exclusive_group()
  354. mode_group.add_argument(
  355. "--hard", action="store_true", help="Reset working tree and index"
  356. )
  357. mode_group.add_argument("--soft", action="store_true", help="Reset only HEAD")
  358. mode_group.add_argument(
  359. "--mixed", action="store_true", help="Reset HEAD and index"
  360. )
  361. parser.add_argument("treeish", nargs="?", help="Commit/tree to reset to")
  362. args = parser.parse_args(args)
  363. if args.hard:
  364. porcelain.reset(".", mode="hard", treeish=args.treeish)
  365. elif args.soft:
  366. # Soft reset: only change HEAD
  367. if args.treeish:
  368. from .repo import Repo
  369. with Repo(".") as repo:
  370. repo.refs[b"HEAD"] = args.treeish.encode()
  371. elif args.mixed:
  372. # Mixed reset is not implemented yet
  373. raise NotImplementedError("Mixed reset not yet implemented")
  374. else:
  375. # Default to mixed behavior (not implemented)
  376. raise NotImplementedError("Mixed reset not yet implemented")
  377. class cmd_daemon(Command):
  378. def run(self, args) -> None:
  379. from dulwich import log_utils
  380. from .protocol import TCP_GIT_PORT
  381. parser = argparse.ArgumentParser()
  382. parser.add_argument(
  383. "-l",
  384. "--listen_address",
  385. default="localhost",
  386. help="Binding IP address.",
  387. )
  388. parser.add_argument(
  389. "-p",
  390. "--port",
  391. type=int,
  392. default=TCP_GIT_PORT,
  393. help="Binding TCP port.",
  394. )
  395. parser.add_argument(
  396. "gitdir", nargs="?", default=".", help="Git directory to serve"
  397. )
  398. args = parser.parse_args(args)
  399. log_utils.default_logging_config()
  400. porcelain.daemon(args.gitdir, address=args.listen_address, port=args.port)
  401. class cmd_web_daemon(Command):
  402. def run(self, args) -> None:
  403. from dulwich import log_utils
  404. parser = argparse.ArgumentParser()
  405. parser.add_argument(
  406. "-l",
  407. "--listen_address",
  408. default="",
  409. help="Binding IP address.",
  410. )
  411. parser.add_argument(
  412. "-p",
  413. "--port",
  414. type=int,
  415. default=8000,
  416. help="Binding TCP port.",
  417. )
  418. parser.add_argument(
  419. "gitdir", nargs="?", default=".", help="Git directory to serve"
  420. )
  421. args = parser.parse_args(args)
  422. log_utils.default_logging_config()
  423. porcelain.web_daemon(args.gitdir, address=args.listen_address, port=args.port)
  424. class cmd_write_tree(Command):
  425. def run(self, args) -> None:
  426. parser = argparse.ArgumentParser()
  427. parser.parse_args(args)
  428. sys.stdout.write("{}\n".format(porcelain.write_tree(".").decode()))
  429. class cmd_receive_pack(Command):
  430. def run(self, args) -> None:
  431. parser = argparse.ArgumentParser()
  432. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  433. args = parser.parse_args(args)
  434. porcelain.receive_pack(args.gitdir)
  435. class cmd_upload_pack(Command):
  436. def run(self, args) -> None:
  437. parser = argparse.ArgumentParser()
  438. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  439. args = parser.parse_args(args)
  440. porcelain.upload_pack(args.gitdir)
  441. class cmd_status(Command):
  442. def run(self, args) -> None:
  443. parser = argparse.ArgumentParser()
  444. parser.add_argument("gitdir", nargs="?", default=".", help="Git directory")
  445. args = parser.parse_args(args)
  446. status = porcelain.status(args.gitdir)
  447. if any(names for (kind, names) in status.staged.items()):
  448. sys.stdout.write("Changes to be committed:\n\n")
  449. for kind, names in status.staged.items():
  450. for name in names:
  451. sys.stdout.write(
  452. f"\t{kind}: {name.decode(sys.getfilesystemencoding())}\n"
  453. )
  454. sys.stdout.write("\n")
  455. if status.unstaged:
  456. sys.stdout.write("Changes not staged for commit:\n\n")
  457. for name in status.unstaged:
  458. sys.stdout.write(f"\t{name.decode(sys.getfilesystemencoding())}\n")
  459. sys.stdout.write("\n")
  460. if status.untracked:
  461. sys.stdout.write("Untracked files:\n\n")
  462. for name in status.untracked:
  463. sys.stdout.write(f"\t{name}\n")
  464. sys.stdout.write("\n")
  465. class cmd_ls_remote(Command):
  466. def run(self, args) -> None:
  467. parser = argparse.ArgumentParser()
  468. parser.add_argument("url", help="Remote URL to list references from")
  469. args = parser.parse_args(args)
  470. refs = porcelain.ls_remote(args.url)
  471. for ref in sorted(refs):
  472. sys.stdout.write(f"{ref}\t{refs[ref]}\n")
  473. class cmd_ls_tree(Command):
  474. def run(self, args) -> None:
  475. parser = argparse.ArgumentParser()
  476. parser.add_argument(
  477. "-r",
  478. "--recursive",
  479. action="store_true",
  480. help="Recursively list tree contents.",
  481. )
  482. parser.add_argument(
  483. "--name-only", action="store_true", help="Only display name."
  484. )
  485. parser.add_argument("treeish", nargs="?", help="Tree-ish to list")
  486. args = parser.parse_args(args)
  487. porcelain.ls_tree(
  488. ".",
  489. args.treeish,
  490. outstream=sys.stdout,
  491. recursive=args.recursive,
  492. name_only=args.name_only,
  493. )
  494. class cmd_pack_objects(Command):
  495. def run(self, args) -> None:
  496. parser = argparse.ArgumentParser()
  497. parser.add_argument(
  498. "--stdout", action="store_true", help="Write pack to stdout"
  499. )
  500. parser.add_argument("--deltify", action="store_true", help="Create deltas")
  501. parser.add_argument(
  502. "--no-reuse-deltas", action="store_true", help="Don't reuse existing deltas"
  503. )
  504. parser.add_argument("basename", nargs="?", help="Base name for pack files")
  505. args = parser.parse_args(args)
  506. if not args.stdout and not args.basename:
  507. parser.error("basename required when not using --stdout")
  508. object_ids = [line.strip() for line in sys.stdin.readlines()]
  509. deltify = args.deltify
  510. reuse_deltas = not args.no_reuse_deltas
  511. if args.stdout:
  512. packf = getattr(sys.stdout, "buffer", sys.stdout)
  513. idxf = None
  514. close = []
  515. else:
  516. packf = open(args.basename + ".pack", "wb")
  517. idxf = open(args.basename + ".idx", "wb")
  518. close = [packf, idxf]
  519. porcelain.pack_objects(
  520. ".", object_ids, packf, idxf, deltify=deltify, reuse_deltas=reuse_deltas
  521. )
  522. for f in close:
  523. f.close()
  524. class cmd_pull(Command):
  525. def run(self, args) -> None:
  526. parser = argparse.ArgumentParser()
  527. parser.add_argument("from_location", type=str)
  528. parser.add_argument("refspec", type=str, nargs="*")
  529. parser.add_argument("--filter", type=str, nargs=1)
  530. parser.add_argument("--protocol", type=int)
  531. args = parser.parse_args(args)
  532. porcelain.pull(
  533. ".",
  534. args.from_location or None,
  535. args.refspec or None,
  536. filter_spec=args.filter,
  537. protocol_version=args.protocol or None,
  538. )
  539. class cmd_push(Command):
  540. def run(self, argv) -> Optional[int]:
  541. parser = argparse.ArgumentParser()
  542. parser.add_argument("-f", "--force", action="store_true", help="Force")
  543. parser.add_argument("to_location", type=str)
  544. parser.add_argument("refspec", type=str, nargs="*")
  545. args = parser.parse_args(argv)
  546. try:
  547. porcelain.push(
  548. ".", args.to_location, args.refspec or None, force=args.force
  549. )
  550. except porcelain.DivergedBranches:
  551. sys.stderr.write("Diverged branches; specify --force to override")
  552. return 1
  553. return None
  554. class cmd_remote_add(Command):
  555. def run(self, args) -> None:
  556. parser = argparse.ArgumentParser()
  557. parser.add_argument("name", help="Name of the remote")
  558. parser.add_argument("url", help="URL of the remote")
  559. args = parser.parse_args(args)
  560. porcelain.remote_add(".", args.name, args.url)
  561. class SuperCommand(Command):
  562. subcommands: ClassVar[dict[str, type[Command]]] = {}
  563. default_command: ClassVar[Optional[type[Command]]] = None
  564. def run(self, args):
  565. if not args:
  566. if self.default_command:
  567. return self.default_command().run(args)
  568. else:
  569. print(
  570. "Supported subcommands: {}".format(
  571. ", ".join(self.subcommands.keys())
  572. )
  573. )
  574. return False
  575. cmd = args[0]
  576. try:
  577. cmd_kls = self.subcommands[cmd]
  578. except KeyError:
  579. print(f"No such subcommand: {args[0]}")
  580. return False
  581. return cmd_kls().run(args[1:])
  582. class cmd_remote(SuperCommand):
  583. subcommands: ClassVar[dict[str, type[Command]]] = {
  584. "add": cmd_remote_add,
  585. }
  586. class cmd_submodule_list(Command):
  587. def run(self, argv) -> None:
  588. parser = argparse.ArgumentParser()
  589. parser.parse_args(argv)
  590. for path, sha in porcelain.submodule_list("."):
  591. sys.stdout.write(f" {sha} {path}\n")
  592. class cmd_submodule_init(Command):
  593. def run(self, argv) -> None:
  594. parser = argparse.ArgumentParser()
  595. parser.parse_args(argv)
  596. porcelain.submodule_init(".")
  597. class cmd_submodule(SuperCommand):
  598. subcommands: ClassVar[dict[str, type[Command]]] = {
  599. "init": cmd_submodule_init,
  600. "list": cmd_submodule_list,
  601. }
  602. default_command = cmd_submodule_list
  603. class cmd_check_ignore(Command):
  604. def run(self, args):
  605. parser = argparse.ArgumentParser()
  606. parser.add_argument("paths", nargs="+", help="Paths to check")
  607. args = parser.parse_args(args)
  608. ret = 1
  609. for path in porcelain.check_ignore(".", args.paths):
  610. print(path)
  611. ret = 0
  612. return ret
  613. class cmd_check_mailmap(Command):
  614. def run(self, args) -> None:
  615. parser = argparse.ArgumentParser()
  616. parser.add_argument("identities", nargs="+", help="Identities to check")
  617. args = parser.parse_args(args)
  618. for identity in args.identities:
  619. canonical_identity = porcelain.check_mailmap(".", identity)
  620. print(canonical_identity)
  621. class cmd_branch(Command):
  622. def run(self, args) -> None:
  623. parser = argparse.ArgumentParser()
  624. parser.add_argument(
  625. "branch",
  626. type=str,
  627. help="Name of the branch",
  628. )
  629. parser.add_argument(
  630. "-d",
  631. "--delete",
  632. action="store_true",
  633. help="Delete branch",
  634. )
  635. args = parser.parse_args(args)
  636. if not args.branch:
  637. print("Usage: dulwich branch [-d] BRANCH_NAME")
  638. sys.exit(1)
  639. if args.delete:
  640. porcelain.branch_delete(".", name=args.branch)
  641. else:
  642. try:
  643. porcelain.branch_create(".", name=args.branch)
  644. except porcelain.Error as e:
  645. sys.stderr.write(f"{e}")
  646. sys.exit(1)
  647. class cmd_checkout(Command):
  648. def run(self, args) -> None:
  649. parser = argparse.ArgumentParser()
  650. parser.add_argument(
  651. "target",
  652. type=str,
  653. help="Name of the branch, tag, or commit to checkout",
  654. )
  655. parser.add_argument(
  656. "-f",
  657. "--force",
  658. action="store_true",
  659. help="Force checkout",
  660. )
  661. parser.add_argument(
  662. "-b",
  663. "--new-branch",
  664. type=str,
  665. help="Create a new branch at the target and switch to it",
  666. )
  667. args = parser.parse_args(args)
  668. if not args.target:
  669. print("Usage: dulwich checkout TARGET [--force] [-b NEW_BRANCH]")
  670. sys.exit(1)
  671. try:
  672. porcelain.checkout(
  673. ".", target=args.target, force=args.force, new_branch=args.new_branch
  674. )
  675. except porcelain.CheckoutError as e:
  676. sys.stderr.write(f"{e}\n")
  677. sys.exit(1)
  678. class cmd_stash_list(Command):
  679. def run(self, args) -> None:
  680. parser = argparse.ArgumentParser()
  681. parser.parse_args(args)
  682. for i, entry in porcelain.stash_list("."):
  683. print("stash@{{{}}}: {}".format(i, entry.message.rstrip("\n")))
  684. class cmd_stash_push(Command):
  685. def run(self, args) -> None:
  686. parser = argparse.ArgumentParser()
  687. parser.parse_args(args)
  688. porcelain.stash_push(".")
  689. print("Saved working directory and index state")
  690. class cmd_stash_pop(Command):
  691. def run(self, args) -> None:
  692. parser = argparse.ArgumentParser()
  693. parser.parse_args(args)
  694. porcelain.stash_pop(".")
  695. print("Restored working directory and index state")
  696. class cmd_stash(SuperCommand):
  697. subcommands: ClassVar[dict[str, type[Command]]] = {
  698. "list": cmd_stash_list,
  699. "pop": cmd_stash_pop,
  700. "push": cmd_stash_push,
  701. }
  702. class cmd_ls_files(Command):
  703. def run(self, args) -> None:
  704. parser = argparse.ArgumentParser()
  705. parser.parse_args(args)
  706. for name in porcelain.ls_files("."):
  707. print(name)
  708. class cmd_describe(Command):
  709. def run(self, args) -> None:
  710. parser = argparse.ArgumentParser()
  711. parser.parse_args(args)
  712. print(porcelain.describe("."))
  713. class cmd_merge(Command):
  714. def run(self, args) -> None:
  715. parser = argparse.ArgumentParser()
  716. parser.add_argument("commit", type=str, help="Commit to merge")
  717. parser.add_argument(
  718. "--no-commit", action="store_true", help="Do not create a merge commit"
  719. )
  720. parser.add_argument(
  721. "--no-ff", action="store_true", help="Force create a merge commit"
  722. )
  723. parser.add_argument("-m", "--message", type=str, help="Merge commit message")
  724. args = parser.parse_args(args)
  725. try:
  726. merge_commit_id, conflicts = porcelain.merge(
  727. ".",
  728. args.commit,
  729. no_commit=args.no_commit,
  730. no_ff=args.no_ff,
  731. message=args.message,
  732. )
  733. if conflicts:
  734. print(f"Merge conflicts in {len(conflicts)} file(s):")
  735. for conflict_path in conflicts:
  736. print(f" {conflict_path.decode()}")
  737. print(
  738. "\nAutomatic merge failed; fix conflicts and then commit the result."
  739. )
  740. sys.exit(1)
  741. elif merge_commit_id is None and not args.no_commit:
  742. print("Already up to date.")
  743. elif args.no_commit:
  744. print("Automatic merge successful; not committing as requested.")
  745. else:
  746. print(
  747. f"Merge successful. Created merge commit {merge_commit_id.decode()}"
  748. )
  749. except porcelain.Error as e:
  750. print(f"Error: {e}")
  751. sys.exit(1)
  752. class cmd_help(Command):
  753. def run(self, args) -> None:
  754. parser = argparse.ArgumentParser()
  755. parser.add_argument(
  756. "-a",
  757. "--all",
  758. action="store_true",
  759. help="List all commands.",
  760. )
  761. args = parser.parse_args(args)
  762. if args.all:
  763. print("Available commands:")
  764. for cmd in sorted(commands):
  765. print(f" {cmd}")
  766. else:
  767. print(
  768. """\
  769. The dulwich command line tool is currently a very basic frontend for the
  770. Dulwich python module. For full functionality, please see the API reference.
  771. For a list of supported commands, see 'dulwich help -a'.
  772. """
  773. )
  774. commands = {
  775. "add": cmd_add,
  776. "archive": cmd_archive,
  777. "branch": cmd_branch,
  778. "check-ignore": cmd_check_ignore,
  779. "check-mailmap": cmd_check_mailmap,
  780. "checkout": cmd_checkout,
  781. "clone": cmd_clone,
  782. "commit": cmd_commit,
  783. "commit-tree": cmd_commit_tree,
  784. "describe": cmd_describe,
  785. "daemon": cmd_daemon,
  786. "diff": cmd_diff,
  787. "diff-tree": cmd_diff_tree,
  788. "dump-pack": cmd_dump_pack,
  789. "dump-index": cmd_dump_index,
  790. "fetch-pack": cmd_fetch_pack,
  791. "fetch": cmd_fetch,
  792. "for-each-ref": cmd_for_each_ref,
  793. "fsck": cmd_fsck,
  794. "help": cmd_help,
  795. "init": cmd_init,
  796. "log": cmd_log,
  797. "ls-files": cmd_ls_files,
  798. "ls-remote": cmd_ls_remote,
  799. "ls-tree": cmd_ls_tree,
  800. "merge": cmd_merge,
  801. "pack-objects": cmd_pack_objects,
  802. "pack-refs": cmd_pack_refs,
  803. "pull": cmd_pull,
  804. "push": cmd_push,
  805. "receive-pack": cmd_receive_pack,
  806. "remote": cmd_remote,
  807. "repack": cmd_repack,
  808. "reset": cmd_reset,
  809. "rev-list": cmd_rev_list,
  810. "rm": cmd_rm,
  811. "show": cmd_show,
  812. "stash": cmd_stash,
  813. "status": cmd_status,
  814. "symbolic-ref": cmd_symbolic_ref,
  815. "submodule": cmd_submodule,
  816. "tag": cmd_tag,
  817. "update-server-info": cmd_update_server_info,
  818. "upload-pack": cmd_upload_pack,
  819. "web-daemon": cmd_web_daemon,
  820. "write-tree": cmd_write_tree,
  821. }
  822. def main(argv=None):
  823. if argv is None:
  824. argv = sys.argv[1:]
  825. if len(argv) < 1:
  826. print("Usage: dulwich <{}> [OPTIONS...]".format("|".join(commands.keys())))
  827. return 1
  828. cmd = argv[0]
  829. try:
  830. cmd_kls = commands[cmd]
  831. except KeyError:
  832. print(f"No such subcommand: {cmd}")
  833. return 1
  834. # TODO(jelmer): Return non-0 on errors
  835. return cmd_kls().run(argv[1:])
  836. def _main() -> None:
  837. if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
  838. signal.signal(signal.SIGQUIT, signal_quit) # type: ignore
  839. signal.signal(signal.SIGINT, signal_int)
  840. sys.exit(main())
  841. if __name__ == "__main__":
  842. _main()