1#!/usr/bin/python -u 2# 3# dulwich - Simple command-line interface to Dulwich 4# Copyright (C) 2008-2011 Jelmer Vernooij <jelmer@jelmer.uk> 5# vim: expandtab 6# 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 24"""Simple command-line interface to Dulwich> 25 26This is a very simple command-line wrapper for Dulwich. It is by 27no means intended to be a full-blown Git command-line interface but just 28a way to test Dulwich. 29""" 30 31import os 32import sys 33from getopt import getopt 34import optparse 35import signal 36 37def signal_int(signal, frame): 38 sys.exit(1) 39 40 41def signal_quit(signal, frame): 42 import pdb 43 pdb.set_trace() 44 45if 'DULWICH_PDB' in os.environ: 46 signal.signal(signal.SIGQUIT, signal_quit) 47signal.signal(signal.SIGINT, signal_int) 48 49from dulwich import porcelain 50from dulwich.client import get_transport_and_path 51from dulwich.errors import ApplyDeltaError 52from dulwich.index import Index 53from dulwich.pack import Pack, sha_to_hex 54from dulwich.patch import write_tree_diff 55from dulwich.repo import Repo 56 57 58class Command(object): 59 """A Dulwich subcommand.""" 60 61 def run(self, args): 62 """Run the command.""" 63 raise NotImplementedError(self.run) 64 65 66class cmd_archive(Command): 67 68 def run(self, args): 69 parser = optparse.OptionParser() 70 parser.add_option("--remote", type=str, 71 help="Retrieve archive from specified remote repo") 72 options, args = parser.parse_args(args) 73 committish = args.pop(0) 74 if options.remote: 75 client, path = get_transport_and_path(options.remote) 76 client.archive(path, committish, sys.stdout.write, 77 write_error=sys.stderr.write) 78 else: 79 porcelain.archive('.', committish, outstream=sys.stdout, 80 errstream=sys.stderr) 81 82 83class cmd_add(Command): 84 85 def run(self, args): 86 opts, args = getopt(args, "", []) 87 88 porcelain.add(".", paths=args) 89 90 91class cmd_rm(Command): 92 93 def run(self, args): 94 opts, args = getopt(args, "", []) 95 96 porcelain.rm(".", paths=args) 97 98 99class cmd_fetch_pack(Command): 100 101 def run(self, args): 102 opts, args = getopt(args, "", ["all"]) 103 opts = dict(opts) 104 client, path = get_transport_and_path(args.pop(0)) 105 r = Repo(".") 106 if "--all" in opts: 107 determine_wants = r.object_store.determine_wants_all 108 else: 109 determine_wants = lambda x: [y for y in args if not y in r.object_store] 110 client.fetch(path, r, determine_wants) 111 112 113class cmd_fetch(Command): 114 115 def run(self, args): 116 opts, args = getopt(args, "", []) 117 opts = dict(opts) 118 client, path = get_transport_and_path(args.pop(0)) 119 r = Repo(".") 120 if "--all" in opts: 121 determine_wants = r.object_store.determine_wants_all 122 refs = client.fetch(path, r, progress=sys.stdout.write) 123 print("Remote refs:") 124 for item in refs.items(): 125 print("%s -> %s" % item) 126 127 128class cmd_fsck(Command): 129 130 def run(self, args): 131 opts, args = getopt(args, "", []) 132 opts = dict(opts) 133 for (obj, msg) in porcelain.fsck('.'): 134 print("%s: %s" % (obj, msg)) 135 136 137class cmd_log(Command): 138 139 def run(self, args): 140 parser = optparse.OptionParser() 141 parser.add_option("--reverse", dest="reverse", action="store_true", 142 help="Reverse order in which entries are printed") 143 parser.add_option("--name-status", dest="name_status", action="store_true", 144 help="Print name/status for each changed file") 145 options, args = parser.parse_args(args) 146 147 porcelain.log(".", paths=args, reverse=options.reverse, 148 name_status=options.name_status, 149 outstream=sys.stdout) 150 151 152class cmd_diff(Command): 153 154 def run(self, args): 155 opts, args = getopt(args, "", []) 156 157 if args == []: 158 print("Usage: dulwich diff COMMITID") 159 sys.exit(1) 160 161 r = Repo(".") 162 commit_id = args[0] 163 commit = r[commit_id] 164 parent_commit = r[commit.parents[0]] 165 write_tree_diff(sys.stdout, r.object_store, parent_commit.tree, commit.tree) 166 167 168class cmd_dump_pack(Command): 169 170 def run(self, args): 171 opts, args = getopt(args, "", []) 172 173 if args == []: 174 print("Usage: dulwich dump-pack FILENAME") 175 sys.exit(1) 176 177 basename, _ = os.path.splitext(args[0]) 178 x = Pack(basename) 179 print("Object names checksum: %s" % x.name()) 180 print("Checksum: %s" % sha_to_hex(x.get_stored_checksum())) 181 if not x.check(): 182 print("CHECKSUM DOES NOT MATCH") 183 print("Length: %d" % len(x)) 184 for name in x: 185 try: 186 print("\t%s" % x[name]) 187 except KeyError as k: 188 print("\t%s: Unable to resolve base %s" % (name, k)) 189 except ApplyDeltaError as e: 190 print("\t%s: Unable to apply delta: %r" % (name, e)) 191 192 193class cmd_dump_index(Command): 194 195 def run(self, args): 196 opts, args = getopt(args, "", []) 197 198 if args == []: 199 print("Usage: dulwich dump-index FILENAME") 200 sys.exit(1) 201 202 filename = args[0] 203 idx = Index(filename) 204 205 for o in idx: 206 print(o, idx[o]) 207 208 209class cmd_init(Command): 210 211 def run(self, args): 212 opts, args = getopt(args, "", ["bare"]) 213 opts = dict(opts) 214 215 if args == []: 216 path = os.getcwd() 217 else: 218 path = args[0] 219 220 porcelain.init(path, bare=("--bare" in opts)) 221 222 223class cmd_clone(Command): 224 225 def run(self, args): 226 parser = optparse.OptionParser() 227 parser.add_option("--bare", dest="bare", 228 help="Whether to create a bare repository.", 229 action="store_true") 230 parser.add_option("--depth", dest="depth", 231 type=int, help="Depth at which to fetch") 232 options, args = parser.parse_args(args) 233 234 if args == []: 235 print("usage: dulwich clone host:path [PATH]") 236 sys.exit(1) 237 238 source = args.pop(0) 239 if len(args) > 0: 240 target = args.pop(0) 241 else: 242 target = None 243 244 porcelain.clone(source, target, bare=options.bare, depth=options.depth) 245 246 247class cmd_commit(Command): 248 249 def run(self, args): 250 opts, args = getopt(args, "", ["message"]) 251 opts = dict(opts) 252 porcelain.commit(".", message=opts["--message"]) 253 254 255class cmd_commit_tree(Command): 256 257 def run(self, args): 258 opts, args = getopt(args, "", ["message"]) 259 if args == []: 260 print("usage: dulwich commit-tree tree") 261 sys.exit(1) 262 opts = dict(opts) 263 porcelain.commit_tree(".", tree=args[0], message=opts["--message"]) 264 265 266class cmd_update_server_info(Command): 267 268 def run(self, args): 269 porcelain.update_server_info(".") 270 271 272class cmd_symbolic_ref(Command): 273 274 def run(self, args): 275 opts, args = getopt(args, "", ["ref-name", "force"]) 276 if not args: 277 print("Usage: dulwich symbolic-ref REF_NAME [--force]") 278 sys.exit(1) 279 280 ref_name = args.pop(0) 281 porcelain.symbolic_ref(".", ref_name=ref_name, force='--force' in args) 282 283 284class cmd_show(Command): 285 286 def run(self, args): 287 opts, args = getopt(args, "", []) 288 porcelain.show(".", args) 289 290 291class cmd_diff_tree(Command): 292 293 def run(self, args): 294 opts, args = getopt(args, "", []) 295 if len(args) < 2: 296 print("Usage: dulwich diff-tree OLD-TREE NEW-TREE") 297 sys.exit(1) 298 porcelain.diff_tree(".", args[0], args[1]) 299 300 301class cmd_rev_list(Command): 302 303 def run(self, args): 304 opts, args = getopt(args, "", []) 305 if len(args) < 1: 306 print('Usage: dulwich rev-list COMMITID...') 307 sys.exit(1) 308 porcelain.rev_list('.', args) 309 310 311class cmd_tag(Command): 312 313 def run(self, args): 314 parser = optparse.OptionParser() 315 parser.add_option("-a", "--annotated", help="Create an annotated tag.", action="store_true") 316 parser.add_option("-s", "--sign", help="Sign the annotated tag.", action="store_true") 317 options, args = parser.parse_args(args) 318 porcelain.tag_create( 319 '.', args[0], annotated=options.annotated, 320 sign=options.sign) 321 322 323class cmd_repack(Command): 324 325 def run(self, args): 326 opts, args = getopt(args, "", []) 327 opts = dict(opts) 328 porcelain.repack('.') 329 330 331class cmd_reset(Command): 332 333 def run(self, args): 334 opts, args = getopt(args, "", ["hard", "soft", "mixed"]) 335 opts = dict(opts) 336 mode = "" 337 if "--hard" in opts: 338 mode = "hard" 339 elif "--soft" in opts: 340 mode = "soft" 341 elif "--mixed" in opts: 342 mode = "mixed" 343 porcelain.reset('.', mode=mode, *args) 344 345 346class cmd_daemon(Command): 347 348 def run(self, args): 349 from dulwich import log_utils 350 from dulwich.protocol import TCP_GIT_PORT 351 parser = optparse.OptionParser() 352 parser.add_option("-l", "--listen_address", dest="listen_address", 353 default="localhost", 354 help="Binding IP address.") 355 parser.add_option("-p", "--port", dest="port", type=int, 356 default=TCP_GIT_PORT, 357 help="Binding TCP port.") 358 options, args = parser.parse_args(args) 359 360 log_utils.default_logging_config() 361 if len(args) >= 1: 362 gitdir = args[0] 363 else: 364 gitdir = '.' 365 from dulwich import porcelain 366 porcelain.daemon(gitdir, address=options.listen_address, 367 port=options.port) 368 369 370class cmd_web_daemon(Command): 371 372 def run(self, args): 373 from dulwich import log_utils 374 parser = optparse.OptionParser() 375 parser.add_option("-l", "--listen_address", dest="listen_address", 376 default="", 377 help="Binding IP address.") 378 parser.add_option("-p", "--port", dest="port", type=int, 379 default=8000, 380 help="Binding TCP port.") 381 options, args = parser.parse_args(args) 382 383 log_utils.default_logging_config() 384 if len(args) >= 1: 385 gitdir = args[0] 386 else: 387 gitdir = '.' 388 from dulwich import porcelain 389 porcelain.web_daemon(gitdir, address=options.listen_address, 390 port=options.port) 391 392 393class cmd_write_tree(Command): 394 395 def run(self, args): 396 parser = optparse.OptionParser() 397 options, args = parser.parse_args(args) 398 sys.stdout.write('%s\n' % porcelain.write_tree('.')) 399 400 401class cmd_receive_pack(Command): 402 403 def run(self, args): 404 parser = optparse.OptionParser() 405 options, args = parser.parse_args(args) 406 if len(args) >= 1: 407 gitdir = args[0] 408 else: 409 gitdir = '.' 410 porcelain.receive_pack(gitdir) 411 412 413class cmd_upload_pack(Command): 414 415 def run(self, args): 416 parser = optparse.OptionParser() 417 options, args = parser.parse_args(args) 418 if len(args) >= 1: 419 gitdir = args[0] 420 else: 421 gitdir = '.' 422 porcelain.upload_pack(gitdir) 423 424 425class cmd_status(Command): 426 427 def run(self, args): 428 parser = optparse.OptionParser() 429 options, args = parser.parse_args(args) 430 if len(args) >= 1: 431 gitdir = args[0] 432 else: 433 gitdir = '.' 434 status = porcelain.status(gitdir) 435 if any(names for (kind, names) in status.staged.items()): 436 sys.stdout.write("Changes to be committed:\n\n") 437 for kind, names in status.staged.items(): 438 for name in names: 439 sys.stdout.write("\t%s: %s\n" % ( 440 kind, name.decode(sys.getfilesystemencoding()))) 441 sys.stdout.write("\n") 442 if status.unstaged: 443 sys.stdout.write("Changes not staged for commit:\n\n") 444 for name in status.unstaged: 445 sys.stdout.write("\t%s\n" % 446 name.decode(sys.getfilesystemencoding())) 447 sys.stdout.write("\n") 448 if status.untracked: 449 sys.stdout.write("Untracked files:\n\n") 450 for name in status.untracked: 451 sys.stdout.write("\t%s\n" % name) 452 sys.stdout.write("\n") 453 454 455class cmd_ls_remote(Command): 456 457 def run(self, args): 458 opts, args = getopt(args, '', []) 459 if len(args) < 1: 460 print('Usage: dulwich ls-remote URL') 461 sys.exit(1) 462 refs = porcelain.ls_remote(args[0]) 463 for ref in sorted(refs): 464 sys.stdout.write("%s\t%s\n" % (ref, refs[ref])) 465 466 467class cmd_ls_tree(Command): 468 469 def run(self, args): 470 parser = optparse.OptionParser() 471 parser.add_option("-r", "--recursive", action="store_true", 472 help="Recusively list tree contents.") 473 parser.add_option("--name-only", action="store_true", 474 help="Only display name.") 475 options, args = parser.parse_args(args) 476 try: 477 treeish = args.pop(0) 478 except IndexError: 479 treeish = None 480 porcelain.ls_tree( 481 '.', treeish, outstream=sys.stdout, recursive=options.recursive, 482 name_only=options.name_only) 483 484 485class cmd_pack_objects(Command): 486 487 def run(self, args): 488 opts, args = getopt(args, '', ['stdout']) 489 opts = dict(opts) 490 if len(args) < 1 and not '--stdout' in args: 491 print('Usage: dulwich pack-objects basename') 492 sys.exit(1) 493 object_ids = [l.strip() for l in sys.stdin.readlines()] 494 basename = args[0] 495 if '--stdout' in opts: 496 packf = getattr(sys.stdout, 'buffer', sys.stdout) 497 idxf = None 498 close = [] 499 else: 500 packf = open(basename + '.pack', 'w') 501 idxf = open(basename + '.idx', 'w') 502 close = [packf, idxf] 503 porcelain.pack_objects('.', object_ids, packf, idxf) 504 for f in close: 505 f.close() 506 507 508class cmd_pull(Command): 509 510 def run(self, args): 511 parser = optparse.OptionParser() 512 options, args = parser.parse_args(args) 513 try: 514 from_location = args[0] 515 except IndexError: 516 from_location = None 517 porcelain.pull('.', from_location) 518 519 520class cmd_push(Command): 521 522 def run(self, args): 523 parser = optparse.OptionParser() 524 options, args = parser.parse_args(args) 525 if len(args) < 2: 526 print("Usage: dulwich push TO-LOCATION REFSPEC..") 527 sys.exit(1) 528 to_location = args[0] 529 refspecs = args[1:] 530 porcelain.push('.', to_location, refspecs) 531 532 533class cmd_remote_add(Command): 534 535 def run(self, args): 536 parser = optparse.OptionParser() 537 options, args = parser.parse_args(args) 538 porcelain.remote_add('.', args[0], args[1]) 539 540 541class SuperCommand(Command): 542 543 subcommands = {} 544 545 def run(self, args): 546 if not args: 547 print("Supported subcommands: %s" % ', '.join(self.subcommands.keys())) 548 return False 549 cmd = args[0] 550 try: 551 cmd_kls = self.subcommands[cmd] 552 except KeyError: 553 print('No such subcommand: %s' % args[0]) 554 return False 555 return cmd_kls().run(args[1:]) 556 557 558class cmd_remote(SuperCommand): 559 560 subcommands = { 561 "add": cmd_remote_add, 562 } 563 564 565class cmd_check_ignore(Command): 566 567 def run(self, args): 568 parser = optparse.OptionParser() 569 options, args = parser.parse_args(args) 570 ret = 1 571 for path in porcelain.check_ignore('.', args): 572 print(path) 573 ret = 0 574 return ret 575 576 577class cmd_check_mailmap(Command): 578 579 def run(self, args): 580 parser = optparse.OptionParser() 581 options, args = parser.parse_args(args) 582 for arg in args: 583 canonical_identity = porcelain.check_mailmap('.', arg) 584 print(canonical_identity) 585 586 587class cmd_stash_list(Command): 588 589 def run(self, args): 590 parser = optparse.OptionParser() 591 options, args = parser.parse_args(args) 592 for i, entry in porcelain.stash_list('.'): 593 print("stash@{%d}: %s" % (i, entry.message.rstrip('\n'))) 594 595 596class cmd_stash_push(Command): 597 598 def run(self, args): 599 parser = optparse.OptionParser() 600 options, args = parser.parse_args(args) 601 porcelain.stash_push('.') 602 print("Saved working directory and index state") 603 604 605class cmd_stash_pop(Command): 606 607 def run(self, args): 608 parser = optparse.OptionParser() 609 options, args = parser.parse_args(args) 610 porcelain.stash_pop('.') 611 print("Restrored working directory and index state") 612 613 614class cmd_stash(SuperCommand): 615 616 subcommands = { 617 "list": cmd_stash_list, 618 "pop": cmd_stash_pop, 619 "push": cmd_stash_push, 620 } 621 622 623class cmd_ls_files(Command): 624 625 def run(self, args): 626 parser = optparse.OptionParser() 627 options, args = parser.parse_args(args) 628 for name in porcelain.ls_files('.'): 629 print(name) 630 631 632class cmd_describe(Command): 633 634 def run(self, args): 635 parser = optparse.OptionParser() 636 options, args = parser.parse_args(args) 637 print(porcelain.describe('.')) 638 639 640class cmd_help(Command): 641 642 def run(self, args): 643 parser = optparse.OptionParser() 644 parser.add_option("-a", "--all", dest="all", 645 action="store_true", 646 help="List all commands.") 647 options, args = parser.parse_args(args) 648 649 if options.all: 650 print('Available commands:') 651 for cmd in sorted(commands): 652 print(' %s' % cmd) 653 else: 654 print("""\ 655The dulwich command line tool is currently a very basic frontend for the 656Dulwich python module. For full functionality, please see the API reference. 657 658For a list of supported commands, see 'dulwich help -a'. 659""") 660 661 662commands = { 663 "add": cmd_add, 664 "archive": cmd_archive, 665 "check-ignore": cmd_check_ignore, 666 "check-mailmap": cmd_check_mailmap, 667 "clone": cmd_clone, 668 "commit": cmd_commit, 669 "commit-tree": cmd_commit_tree, 670 "describe": cmd_describe, 671 "daemon": cmd_daemon, 672 "diff": cmd_diff, 673 "diff-tree": cmd_diff_tree, 674 "dump-pack": cmd_dump_pack, 675 "dump-index": cmd_dump_index, 676 "fetch-pack": cmd_fetch_pack, 677 "fetch": cmd_fetch, 678 "fsck": cmd_fsck, 679 "help": cmd_help, 680 "init": cmd_init, 681 "log": cmd_log, 682 "ls-files": cmd_ls_files, 683 "ls-remote": cmd_ls_remote, 684 "ls-tree": cmd_ls_tree, 685 "pack-objects": cmd_pack_objects, 686 "pull": cmd_pull, 687 "push": cmd_push, 688 "receive-pack": cmd_receive_pack, 689 "remote": cmd_remote, 690 "repack": cmd_repack, 691 "reset": cmd_reset, 692 "rev-list": cmd_rev_list, 693 "rm": cmd_rm, 694 "show": cmd_show, 695 "stash": cmd_stash, 696 "status": cmd_status, 697 "symbolic-ref": cmd_symbolic_ref, 698 "tag": cmd_tag, 699 "update-server-info": cmd_update_server_info, 700 "upload-pack": cmd_upload_pack, 701 "web-daemon": cmd_web_daemon, 702 "write-tree": cmd_write_tree, 703 } 704 705if len(sys.argv) < 2: 706 print("Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys()))) 707 sys.exit(1) 708 709cmd = sys.argv[1] 710try: 711 cmd_kls = commands[cmd] 712except KeyError: 713 print("No such subcommand: %s" % cmd) 714 sys.exit(1) 715# TODO(jelmer): Return non-0 on errors 716cmd_kls().run(sys.argv[2:]) 717