Add support for creating signed tags.
[jelmer/dulwich.git] / bin / dulwich
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
26 This is a very simple command-line wrapper for Dulwich. It is by
27 no means intended to be a full-blown Git command-line interface but just
28 a way to test Dulwich.
29 """
30
31 import os
32 import sys
33 from getopt import getopt
34 import optparse
35 import signal
36
37 def signal_int(signal, frame):
38     sys.exit(1)
39
40
41 def signal_quit(signal, frame):
42     import pdb
43     pdb.set_trace()
44
45 if 'DULWICH_PDB' in os.environ:
46     signal.signal(signal.SIGQUIT, signal_quit)
47 signal.signal(signal.SIGINT, signal_int)
48
49 from dulwich import porcelain
50 from dulwich.client import get_transport_and_path
51 from dulwich.errors import ApplyDeltaError
52 from dulwich.index import Index
53 from dulwich.pack import Pack, sha_to_hex
54 from dulwich.patch import write_tree_diff
55 from dulwich.repo import Repo
56
57
58 class Command(object):
59     """A Dulwich subcommand."""
60
61     def run(self, args):
62         """Run the command."""
63         raise NotImplementedError(self.run)
64
65
66 class 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
83 class cmd_add(Command):
84
85     def run(self, args):
86         opts, args = getopt(args, "", [])
87
88         porcelain.add(".", paths=args)
89
90
91 class cmd_rm(Command):
92
93     def run(self, args):
94         opts, args = getopt(args, "", [])
95
96         porcelain.rm(".", paths=args)
97
98
99 class 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
113 class 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
128 class 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
137 class 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
152 class 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
168 class 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
193 class 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
209 class 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
223 class 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
247 class 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
255 class 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
266 class cmd_update_server_info(Command):
267
268     def run(self, args):
269         porcelain.update_server_info(".")
270
271
272 class 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
284 class cmd_show(Command):
285
286     def run(self, args):
287         opts, args = getopt(args, "", [])
288         porcelain.show(".", args)
289
290
291 class 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
301 class 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
311 class 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
323 class cmd_repack(Command):
324
325     def run(self, args):
326         opts, args = getopt(args, "", [])
327         opts = dict(opts)
328         porcelain.repack('.')
329
330
331 class 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
346 class 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
370 class 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
393 class 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
401 class 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
413 class 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
425 class 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
455 class 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
467 class 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
485 class 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
508 class 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
520 class 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
533 class 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
541 class 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
558 class cmd_remote(SuperCommand):
559
560     subcommands = {
561         "add": cmd_remote_add,
562     }
563
564
565 class 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
577 class 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
587 class 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
596 class 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
605 class 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
614 class cmd_stash(SuperCommand):
615
616     subcommands = {
617         "list": cmd_stash_list,
618         "pop": cmd_stash_pop,
619         "push": cmd_stash_push,
620     }
621
622
623 class 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
632 class 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
640 class 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("""\
655 The dulwich command line tool is currently a very basic frontend for the
656 Dulwich python module. For full functionality, please see the API reference.
657
658 For a list of supported commands, see 'dulwich help -a'.
659 """)
660
661
662 commands = {
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
705 if len(sys.argv) < 2:
706     print("Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys())))
707     sys.exit(1)
708
709 cmd = sys.argv[1]
710 try:
711     cmd_kls = commands[cmd]
712 except KeyError:
713     print("No such subcommand: %s" % cmd)
714     sys.exit(1)
715 # TODO(jelmer): Return non-0 on errors
716 cmd_kls().run(sys.argv[2:])