cli.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  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_unpack_objects(Command):
  525. def run(self, args) -> None:
  526. parser = argparse.ArgumentParser()
  527. parser.add_argument("pack_file", help="Pack file to unpack")
  528. args = parser.parse_args(args)
  529. count = porcelain.unpack_objects(args.pack_file)
  530. print(f"Unpacked {count} objects")
  531. class cmd_pull(Command):
  532. def run(self, args) -> None:
  533. parser = argparse.ArgumentParser()
  534. parser.add_argument("from_location", type=str)
  535. parser.add_argument("refspec", type=str, nargs="*")
  536. parser.add_argument("--filter", type=str, nargs=1)
  537. parser.add_argument("--protocol", type=int)
  538. args = parser.parse_args(args)
  539. porcelain.pull(
  540. ".",
  541. args.from_location or None,
  542. args.refspec or None,
  543. filter_spec=args.filter,
  544. protocol_version=args.protocol or None,
  545. )
  546. class cmd_push(Command):
  547. def run(self, argv) -> Optional[int]:
  548. parser = argparse.ArgumentParser()
  549. parser.add_argument("-f", "--force", action="store_true", help="Force")
  550. parser.add_argument("to_location", type=str)
  551. parser.add_argument("refspec", type=str, nargs="*")
  552. args = parser.parse_args(argv)
  553. try:
  554. porcelain.push(
  555. ".", args.to_location, args.refspec or None, force=args.force
  556. )
  557. except porcelain.DivergedBranches:
  558. sys.stderr.write("Diverged branches; specify --force to override")
  559. return 1
  560. return None
  561. class cmd_remote_add(Command):
  562. def run(self, args) -> None:
  563. parser = argparse.ArgumentParser()
  564. parser.add_argument("name", help="Name of the remote")
  565. parser.add_argument("url", help="URL of the remote")
  566. args = parser.parse_args(args)
  567. porcelain.remote_add(".", args.name, args.url)
  568. class SuperCommand(Command):
  569. subcommands: ClassVar[dict[str, type[Command]]] = {}
  570. default_command: ClassVar[Optional[type[Command]]] = None
  571. def run(self, args):
  572. if not args:
  573. if self.default_command:
  574. return self.default_command().run(args)
  575. else:
  576. print(
  577. "Supported subcommands: {}".format(
  578. ", ".join(self.subcommands.keys())
  579. )
  580. )
  581. return False
  582. cmd = args[0]
  583. try:
  584. cmd_kls = self.subcommands[cmd]
  585. except KeyError:
  586. print(f"No such subcommand: {args[0]}")
  587. return False
  588. return cmd_kls().run(args[1:])
  589. class cmd_remote(SuperCommand):
  590. subcommands: ClassVar[dict[str, type[Command]]] = {
  591. "add": cmd_remote_add,
  592. }
  593. class cmd_submodule_list(Command):
  594. def run(self, argv) -> None:
  595. parser = argparse.ArgumentParser()
  596. parser.parse_args(argv)
  597. for path, sha in porcelain.submodule_list("."):
  598. sys.stdout.write(f" {sha} {path}\n")
  599. class cmd_submodule_init(Command):
  600. def run(self, argv) -> None:
  601. parser = argparse.ArgumentParser()
  602. parser.parse_args(argv)
  603. porcelain.submodule_init(".")
  604. class cmd_submodule(SuperCommand):
  605. subcommands: ClassVar[dict[str, type[Command]]] = {
  606. "init": cmd_submodule_init,
  607. "list": cmd_submodule_list,
  608. }
  609. default_command = cmd_submodule_list
  610. class cmd_check_ignore(Command):
  611. def run(self, args):
  612. parser = argparse.ArgumentParser()
  613. parser.add_argument("paths", nargs="+", help="Paths to check")
  614. args = parser.parse_args(args)
  615. ret = 1
  616. for path in porcelain.check_ignore(".", args.paths):
  617. print(path)
  618. ret = 0
  619. return ret
  620. class cmd_check_mailmap(Command):
  621. def run(self, args) -> None:
  622. parser = argparse.ArgumentParser()
  623. parser.add_argument("identities", nargs="+", help="Identities to check")
  624. args = parser.parse_args(args)
  625. for identity in args.identities:
  626. canonical_identity = porcelain.check_mailmap(".", identity)
  627. print(canonical_identity)
  628. class cmd_branch(Command):
  629. def run(self, args) -> None:
  630. parser = argparse.ArgumentParser()
  631. parser.add_argument(
  632. "branch",
  633. type=str,
  634. help="Name of the branch",
  635. )
  636. parser.add_argument(
  637. "-d",
  638. "--delete",
  639. action="store_true",
  640. help="Delete branch",
  641. )
  642. args = parser.parse_args(args)
  643. if not args.branch:
  644. print("Usage: dulwich branch [-d] BRANCH_NAME")
  645. sys.exit(1)
  646. if args.delete:
  647. porcelain.branch_delete(".", name=args.branch)
  648. else:
  649. try:
  650. porcelain.branch_create(".", name=args.branch)
  651. except porcelain.Error as e:
  652. sys.stderr.write(f"{e}")
  653. sys.exit(1)
  654. class cmd_checkout(Command):
  655. def run(self, args) -> None:
  656. parser = argparse.ArgumentParser()
  657. parser.add_argument(
  658. "target",
  659. type=str,
  660. help="Name of the branch, tag, or commit to checkout",
  661. )
  662. parser.add_argument(
  663. "-f",
  664. "--force",
  665. action="store_true",
  666. help="Force checkout",
  667. )
  668. parser.add_argument(
  669. "-b",
  670. "--new-branch",
  671. type=str,
  672. help="Create a new branch at the target and switch to it",
  673. )
  674. args = parser.parse_args(args)
  675. if not args.target:
  676. print("Usage: dulwich checkout TARGET [--force] [-b NEW_BRANCH]")
  677. sys.exit(1)
  678. try:
  679. porcelain.checkout(
  680. ".", target=args.target, force=args.force, new_branch=args.new_branch
  681. )
  682. except porcelain.CheckoutError as e:
  683. sys.stderr.write(f"{e}\n")
  684. sys.exit(1)
  685. class cmd_stash_list(Command):
  686. def run(self, args) -> None:
  687. parser = argparse.ArgumentParser()
  688. parser.parse_args(args)
  689. for i, entry in porcelain.stash_list("."):
  690. print("stash@{{{}}}: {}".format(i, entry.message.rstrip("\n")))
  691. class cmd_stash_push(Command):
  692. def run(self, args) -> None:
  693. parser = argparse.ArgumentParser()
  694. parser.parse_args(args)
  695. porcelain.stash_push(".")
  696. print("Saved working directory and index state")
  697. class cmd_stash_pop(Command):
  698. def run(self, args) -> None:
  699. parser = argparse.ArgumentParser()
  700. parser.parse_args(args)
  701. porcelain.stash_pop(".")
  702. print("Restored working directory and index state")
  703. class cmd_stash(SuperCommand):
  704. subcommands: ClassVar[dict[str, type[Command]]] = {
  705. "list": cmd_stash_list,
  706. "pop": cmd_stash_pop,
  707. "push": cmd_stash_push,
  708. }
  709. class cmd_ls_files(Command):
  710. def run(self, args) -> None:
  711. parser = argparse.ArgumentParser()
  712. parser.parse_args(args)
  713. for name in porcelain.ls_files("."):
  714. print(name)
  715. class cmd_describe(Command):
  716. def run(self, args) -> None:
  717. parser = argparse.ArgumentParser()
  718. parser.parse_args(args)
  719. print(porcelain.describe("."))
  720. class cmd_merge(Command):
  721. def run(self, args) -> Optional[int]:
  722. parser = argparse.ArgumentParser()
  723. parser.add_argument("commit", type=str, help="Commit to merge")
  724. parser.add_argument(
  725. "--no-commit", action="store_true", help="Do not create a merge commit"
  726. )
  727. parser.add_argument(
  728. "--no-ff", action="store_true", help="Force create a merge commit"
  729. )
  730. parser.add_argument("-m", "--message", type=str, help="Merge commit message")
  731. args = parser.parse_args(args)
  732. try:
  733. merge_commit_id, conflicts = porcelain.merge(
  734. ".",
  735. args.commit,
  736. no_commit=args.no_commit,
  737. no_ff=args.no_ff,
  738. message=args.message,
  739. )
  740. if conflicts:
  741. print(f"Merge conflicts in {len(conflicts)} file(s):")
  742. for conflict_path in conflicts:
  743. print(f" {conflict_path.decode()}")
  744. print(
  745. "\nAutomatic merge failed; fix conflicts and then commit the result."
  746. )
  747. return 1
  748. elif merge_commit_id is None and not args.no_commit:
  749. print("Already up to date.")
  750. elif args.no_commit:
  751. print("Automatic merge successful; not committing as requested.")
  752. else:
  753. print(
  754. f"Merge successful. Created merge commit {merge_commit_id.decode()}"
  755. )
  756. return None
  757. except porcelain.Error as e:
  758. print(f"Error: {e}")
  759. return 1
  760. class cmd_merge_tree(Command):
  761. def run(self, args) -> Optional[int]:
  762. parser = argparse.ArgumentParser(
  763. description="Perform a tree-level merge without touching the working directory"
  764. )
  765. parser.add_argument(
  766. "base_tree",
  767. nargs="?",
  768. help="The common ancestor tree (optional, defaults to empty tree)",
  769. )
  770. parser.add_argument("our_tree", help="Our side of the merge")
  771. parser.add_argument("their_tree", help="Their side of the merge")
  772. parser.add_argument(
  773. "-z",
  774. "--name-only",
  775. action="store_true",
  776. help="Output only conflict paths, null-terminated",
  777. )
  778. args = parser.parse_args(args)
  779. try:
  780. # Determine base tree - if only two args provided, base is None
  781. if args.base_tree is None:
  782. # Only two arguments provided
  783. base_tree = None
  784. our_tree = args.our_tree
  785. their_tree = args.their_tree
  786. else:
  787. # Three arguments provided
  788. base_tree = args.base_tree
  789. our_tree = args.our_tree
  790. their_tree = args.their_tree
  791. merged_tree_id, conflicts = porcelain.merge_tree(
  792. ".", base_tree, our_tree, their_tree
  793. )
  794. if args.name_only:
  795. # Output only conflict paths, null-terminated
  796. for conflict_path in conflicts:
  797. sys.stdout.buffer.write(conflict_path)
  798. sys.stdout.buffer.write(b"\0")
  799. else:
  800. # Output the merged tree SHA
  801. print(merged_tree_id.decode("ascii"))
  802. # Output conflict information
  803. if conflicts:
  804. print(f"\nConflicts in {len(conflicts)} file(s):")
  805. for conflict_path in conflicts:
  806. print(f" {conflict_path.decode()}")
  807. return None
  808. except porcelain.Error as e:
  809. print(f"Error: {e}", file=sys.stderr)
  810. return 1
  811. except KeyError as e:
  812. print(f"Error: Object not found: {e}", file=sys.stderr)
  813. return 1
  814. class cmd_help(Command):
  815. def run(self, args) -> None:
  816. parser = argparse.ArgumentParser()
  817. parser.add_argument(
  818. "-a",
  819. "--all",
  820. action="store_true",
  821. help="List all commands.",
  822. )
  823. args = parser.parse_args(args)
  824. if args.all:
  825. print("Available commands:")
  826. for cmd in sorted(commands):
  827. print(f" {cmd}")
  828. else:
  829. print(
  830. """\
  831. The dulwich command line tool is currently a very basic frontend for the
  832. Dulwich python module. For full functionality, please see the API reference.
  833. For a list of supported commands, see 'dulwich help -a'.
  834. """
  835. )
  836. commands = {
  837. "add": cmd_add,
  838. "archive": cmd_archive,
  839. "branch": cmd_branch,
  840. "check-ignore": cmd_check_ignore,
  841. "check-mailmap": cmd_check_mailmap,
  842. "checkout": cmd_checkout,
  843. "clone": cmd_clone,
  844. "commit": cmd_commit,
  845. "commit-tree": cmd_commit_tree,
  846. "describe": cmd_describe,
  847. "daemon": cmd_daemon,
  848. "diff": cmd_diff,
  849. "diff-tree": cmd_diff_tree,
  850. "dump-pack": cmd_dump_pack,
  851. "dump-index": cmd_dump_index,
  852. "fetch-pack": cmd_fetch_pack,
  853. "fetch": cmd_fetch,
  854. "for-each-ref": cmd_for_each_ref,
  855. "fsck": cmd_fsck,
  856. "help": cmd_help,
  857. "init": cmd_init,
  858. "log": cmd_log,
  859. "ls-files": cmd_ls_files,
  860. "ls-remote": cmd_ls_remote,
  861. "ls-tree": cmd_ls_tree,
  862. "merge": cmd_merge,
  863. "merge-tree": cmd_merge_tree,
  864. "pack-objects": cmd_pack_objects,
  865. "pack-refs": cmd_pack_refs,
  866. "pull": cmd_pull,
  867. "push": cmd_push,
  868. "receive-pack": cmd_receive_pack,
  869. "remote": cmd_remote,
  870. "repack": cmd_repack,
  871. "reset": cmd_reset,
  872. "rev-list": cmd_rev_list,
  873. "rm": cmd_rm,
  874. "show": cmd_show,
  875. "stash": cmd_stash,
  876. "status": cmd_status,
  877. "symbolic-ref": cmd_symbolic_ref,
  878. "submodule": cmd_submodule,
  879. "tag": cmd_tag,
  880. "unpack-objects": cmd_unpack_objects,
  881. "update-server-info": cmd_update_server_info,
  882. "upload-pack": cmd_upload_pack,
  883. "web-daemon": cmd_web_daemon,
  884. "write-tree": cmd_write_tree,
  885. }
  886. def main(argv=None):
  887. if argv is None:
  888. argv = sys.argv[1:]
  889. if len(argv) < 1:
  890. print("Usage: dulwich <{}> [OPTIONS...]".format("|".join(commands.keys())))
  891. return 1
  892. cmd = argv[0]
  893. try:
  894. cmd_kls = commands[cmd]
  895. except KeyError:
  896. print(f"No such subcommand: {cmd}")
  897. return 1
  898. # TODO(jelmer): Return non-0 on errors
  899. return cmd_kls().run(argv[1:])
  900. def _main() -> None:
  901. if "DULWICH_PDB" in os.environ and getattr(signal, "SIGQUIT", None):
  902. signal.signal(signal.SIGQUIT, signal_quit) # type: ignore
  903. signal.signal(signal.SIGINT, signal_int)
  904. sys.exit(main())
  905. if __name__ == "__main__":
  906. _main()