Merge pull request #1391 from davvid/macos/hotkeys
[git-cola.git] / cola / main.py
blob63b603cfd6a815649355bd6d19894c2a3c878d7a
1 """Launcher and command line interface to git-cola"""
2 import argparse
3 import sys
5 from . import app
6 from . import cmds
7 from . import compat
8 from . import core
11 def main(argv=None):
12 app.initialize()
13 if argv is None:
14 argv = sys.argv[1:]
15 # we're using argparse with subparsers, but argparse
16 # does not allow us to assign a default subparser
17 # when none has been specified. We fake it by injecting
18 # 'cola' into the command-line so that parse_args()
19 # routes them to the 'cola' parser by default.
20 help_commands = core.encode('--help-commands')
21 args = [core.encode(arg) for arg in argv]
22 if not argv or argv[0].startswith('-') and help_commands not in args:
23 argv.insert(0, 'cola')
24 elif help_commands in argv:
25 argv.append('--help')
26 args = parse_args(argv)
27 return args.func(args)
30 def parse_args(argv):
31 parser = argparse.ArgumentParser()
32 # Newer versions of argparse (Python 3.6+) emit an error message for
33 # "--help-commands" unless we register the flag on the main parser.
34 if compat.PY_VERSION >= (3, 6):
35 add_help_options(parser)
36 parser.set_defaults(func=lambda _: parser.print_help())
38 subparser = parser.add_subparsers(title='valid commands')
39 add_cola_command(subparser)
40 add_about_command(subparser)
41 add_am_command(subparser)
42 add_archive_command(subparser)
43 add_branch_command(subparser)
44 add_browse_command(subparser)
45 add_clone_command(subparser)
46 add_config_command(subparser)
47 add_dag_command(subparser)
48 add_diff_command(subparser)
49 add_fetch_command(subparser)
50 add_find_command(subparser)
51 add_grep_command(subparser)
52 add_merge_command(subparser)
53 add_pull_command(subparser)
54 add_push_command(subparser)
55 add_rebase_command(subparser)
56 add_recent_command(subparser)
57 add_remote_command(subparser)
58 add_search_command(subparser)
59 add_stash_command(subparser)
60 add_tag_command(subparser)
61 add_version_command(subparser)
63 return parser.parse_args(argv)
66 def add_help_options(parser):
67 """Add the --help-commands flag to the parser"""
68 parser.add_argument(
69 '--help-commands',
70 default=False,
71 action='store_true',
72 help='show available sub-commands',
76 def add_command(parent, name, description, func):
77 parser = parent.add_parser(str(name), help=description)
78 parser.set_defaults(func=func)
79 app.add_common_arguments(parser)
80 return parser
83 def add_cola_command(subparser):
84 parser = add_command(subparser, 'cola', 'start git-cola', cmd_cola)
85 parser.add_argument(
86 '--amend', default=False, action='store_true', help='start in amend mode'
88 add_help_options(parser)
89 parser.add_argument(
90 '--status-filter', '-s', metavar='<path>', default='', help='status path filter'
94 def add_about_command(parent):
95 add_command(parent, 'about', 'about git-cola', cmd_about)
98 def add_am_command(parent):
99 parser = add_command(parent, 'am', 'apply patches using "git am"', cmd_am)
100 parser.add_argument(
101 'patches', metavar='<patches>', nargs='*', help='patches to apply'
105 def add_archive_command(parent):
106 parser = add_command(parent, 'archive', 'save an archive', cmd_archive)
107 parser.add_argument(
108 'ref', metavar='<ref>', nargs='?', default=None, help='commit to archive'
112 def add_branch_command(subparser):
113 add_command(subparser, 'branch', 'create a branch', cmd_branch)
116 def add_browse_command(subparser):
117 add_command(subparser, 'browse', 'browse repository', cmd_browse)
120 def add_clone_command(subparser):
121 add_command(subparser, 'clone', 'clone repository', cmd_clone)
124 def add_config_command(subparser):
125 add_command(subparser, 'config', 'edit configuration', cmd_config)
128 def add_dag_command(subparser):
129 parser = add_command(subparser, 'dag', 'start git-dag', cmd_dag)
130 parser.add_argument(
131 '-c',
132 '--count',
133 metavar='<count>',
134 type=int,
135 default=1000,
136 help='number of commits to display',
138 parser.add_argument(
139 '--all',
140 action='store_true',
141 dest='show_all',
142 help='visualize all branches',
143 default=False,
145 parser.add_argument(
146 'args', nargs=argparse.REMAINDER, metavar='<args>', help='git log arguments'
150 def add_diff_command(subparser):
151 parser = add_command(subparser, 'diff', 'view diffs', cmd_diff)
152 parser.add_argument(
153 'args', nargs=argparse.REMAINDER, metavar='<args>', help='git diff arguments'
157 def add_fetch_command(subparser):
158 add_command(subparser, 'fetch', 'fetch remotes', cmd_fetch)
161 def add_find_command(subparser):
162 parser = add_command(subparser, 'find', 'find files', cmd_find)
163 parser.add_argument('paths', nargs='*', metavar='<path>', help='filter by path')
166 def add_grep_command(subparser):
167 parser = add_command(subparser, 'grep', 'grep source', cmd_grep)
168 parser.add_argument('args', nargs='*', metavar='<args>', help='git grep arguments')
171 def add_merge_command(subparser):
172 parser = add_command(subparser, 'merge', 'merge branches', cmd_merge)
173 parser.add_argument(
174 'ref', nargs='?', metavar='<ref>', help='branch, tag, or commit to merge'
178 def add_pull_command(subparser):
179 parser = add_command(subparser, 'pull', 'pull remote branches', cmd_pull)
180 parser.add_argument(
181 '--rebase',
182 default=False,
183 action='store_true',
184 help='rebase local branch when pulling',
188 def add_push_command(subparser):
189 add_command(subparser, 'push', 'push remote branches', cmd_push)
192 def add_rebase_command(subparser):
193 parser = add_command(subparser, 'rebase', 'interactive rebase', cmd_rebase)
194 parser.add_argument(
195 '-v',
196 '--verbose',
197 default=False,
198 action='store_true',
199 help='display a diffstat of what changed upstream',
201 parser.add_argument(
202 '-q',
203 '--quiet',
204 default=False,
205 action='store_true',
206 help='be quiet. implies --no-stat',
208 parser.add_argument(
209 '-i', '--interactive', default=True, action='store_true', help=argparse.SUPPRESS
211 parser.add_argument(
212 '--autostash',
213 default=False,
214 action='store_true',
215 help='automatically stash/stash pop before and after',
217 parser.add_argument(
218 '--fork-point',
219 default=False,
220 action='store_true',
221 help="use 'merge-base --fork-point' to refine upstream",
223 parser.add_argument(
224 '--onto',
225 default=None,
226 metavar='<newbase>',
227 help='rebase onto given branch instead of upstream',
229 parser.add_argument(
230 '-p',
231 '--preserve-merges',
232 default=False,
233 action='store_true',
234 help='try to recreate merges instead of ignoring them',
236 parser.add_argument(
237 '-s',
238 '--strategy',
239 default=None,
240 metavar='<strategy>',
241 help='use the given merge strategy',
243 parser.add_argument(
244 '--no-ff',
245 default=False,
246 action='store_true',
247 help='cherry-pick all commits, even if unchanged',
249 parser.add_argument(
250 '-m',
251 '--merge',
252 default=False,
253 action='store_true',
254 help='use merging strategies to rebase',
256 parser.add_argument(
257 '-x',
258 '--exec',
259 default=None,
260 help='add exec lines after each commit of ' 'the editable list',
262 parser.add_argument(
263 '-k',
264 '--keep-empty',
265 default=False,
266 action='store_true',
267 help='preserve empty commits during rebase',
269 parser.add_argument(
270 '-f',
271 '--force-rebase',
272 default=False,
273 action='store_true',
274 help='force rebase even if branch is up to date',
276 parser.add_argument(
277 '-X',
278 '--strategy-option',
279 default=None,
280 metavar='<arg>',
281 help='pass the argument through to the merge strategy',
283 parser.add_argument(
284 '--stat',
285 default=False,
286 action='store_true',
287 help='display a diffstat of what changed upstream',
289 parser.add_argument(
290 '-n',
291 '--no-stat',
292 default=False,
293 action='store_true',
294 help='do not show diffstat of what changed upstream',
296 parser.add_argument(
297 '--verify',
298 default=False,
299 action='store_true',
300 help='allow pre-rebase hook to run',
302 parser.add_argument(
303 '--rerere-autoupdate',
304 default=False,
305 action='store_true',
306 help='allow rerere to update index with ' 'resolved conflicts',
308 parser.add_argument(
309 '--root',
310 default=False,
311 action='store_true',
312 help='rebase all reachable commits up to the root(s)',
314 parser.add_argument(
315 '--autosquash',
316 default=True,
317 action='store_true',
318 help='move commits that begin with ' 'squash!/fixup! under -i',
320 parser.add_argument(
321 '--no-autosquash',
322 default=True,
323 action='store_false',
324 dest='autosquash',
325 help='do not move commits that begin with ' 'squash!/fixup! under -i',
327 parser.add_argument(
328 '--committer-date-is-author-date',
329 default=False,
330 action='store_true',
331 help="passed to 'git am' by 'git rebase'",
333 parser.add_argument(
334 '--ignore-date',
335 default=False,
336 action='store_true',
337 help="passed to 'git am' by 'git rebase'",
339 parser.add_argument(
340 '--whitespace',
341 default=False,
342 action='store_true',
343 help="passed to 'git apply' by 'git rebase'",
345 parser.add_argument(
346 '--ignore-whitespace',
347 default=False,
348 action='store_true',
349 help="passed to 'git apply' by 'git rebase'",
351 parser.add_argument(
352 '--update-refs',
353 default=False,
354 action='store_true',
355 help='update branches that point to commits that are being rebased',
357 parser.add_argument(
358 '-C',
359 dest='context_lines',
360 default=None,
361 metavar='<n>',
362 help="passed to 'git apply' by 'git rebase'",
365 actions = parser.add_argument_group('actions')
366 actions.add_argument(
367 '--continue', default=False, action='store_true', help='continue'
369 actions.add_argument(
370 '--abort',
371 default=False,
372 action='store_true',
373 help='abort and check out the original branch',
375 actions.add_argument(
376 '--skip',
377 default=False,
378 action='store_true',
379 help='skip current patch and continue',
381 actions.add_argument(
382 '--edit-todo',
383 default=False,
384 action='store_true',
385 help='edit the todo list during an interactive rebase',
388 parser.add_argument(
389 'upstream',
390 nargs='?',
391 default=None,
392 metavar='<upstream>',
393 help='the upstream configured in branch.<name>.remote '
394 'and branch.<name>.merge options will be used '
395 'when <upstream> is omitted; see git-rebase(1) '
396 'for details. If you are currently not on any '
397 'branch or if the current branch does not have '
398 'a configured upstream, the rebase will abort',
400 parser.add_argument(
401 'branch',
402 nargs='?',
403 default=None,
404 metavar='<branch>',
405 help='git rebase will perform an automatic '
406 '"git checkout <branch>" before doing anything '
407 'else when <branch> is specified',
411 def add_recent_command(subparser):
412 add_command(subparser, 'recent', 'edit recent files', cmd_recent)
415 def add_remote_command(subparser):
416 add_command(subparser, 'remote', 'edit remotes', cmd_remote)
419 def add_search_command(subparser):
420 add_command(subparser, 'search', 'search commits', cmd_search)
423 def add_stash_command(subparser):
424 add_command(subparser, 'stash', 'stash and unstash changes', cmd_stash)
427 def add_tag_command(subparser):
428 parser = add_command(subparser, 'tag', 'create tags', cmd_tag)
429 parser.add_argument(
430 'name', metavar='<name>', nargs='?', default=None, help='tag name'
432 parser.add_argument(
433 'ref', metavar='<ref>', nargs='?', default=None, help='commit to tag'
435 parser.add_argument(
436 '-s',
437 '--sign',
438 default=False,
439 action='store_true',
440 help='annotated and GPG-signed tag',
444 def add_version_command(subparser):
445 parser = add_command(subparser, 'version', 'print the version', cmd_version)
446 parser.add_argument(
447 '--builtin',
448 action='store_true',
449 default=False,
450 help=argparse.SUPPRESS,
452 parser.add_argument(
453 '--brief',
454 action='store_true',
455 default=False,
456 help='print the version number only',
460 # entry points
461 def cmd_cola(args):
462 from .widgets.main import MainView
464 status_filter = args.status_filter
465 if status_filter:
466 status_filter = core.abspath(status_filter)
468 context = app.application_init(args)
470 context.timer.start('view')
471 view = MainView(context)
472 if args.amend:
473 cmds.do(cmds.AmendMode, context, amend=True)
475 if status_filter:
476 view.set_filter(core.relpath(status_filter))
478 context.timer.stop('view')
479 if args.perf:
480 context.timer.display('view')
482 return app.application_run(context, view, start=start_cola, stop=app.default_stop)
485 def start_cola(context, view):
486 app.default_start(context, view)
487 view.start(context)
490 def cmd_about(args):
491 from .widgets import about
493 context = app.application_init(args)
494 view = about.about_dialog(context)
495 return app.application_start(context, view)
498 def cmd_am(args):
499 from .widgets.patch import new_apply_patches
501 context = app.application_init(args)
502 view = new_apply_patches(context, patches=args.patches)
503 return app.application_start(context, view)
506 def cmd_archive(args):
507 from .widgets import archive
509 context = app.application_init(args, update=True)
510 if args.ref is None:
511 args.ref = context.model.currentbranch
512 view = archive.Archive(context, args.ref)
513 return app.application_start(context, view)
516 def cmd_branch(args):
517 from .widgets.createbranch import create_new_branch
519 context = app.application_init(args, update=True)
520 view = create_new_branch(context)
521 return app.application_start(context, view)
524 def cmd_browse(args):
525 from .widgets.browse import worktree_browser
527 context = app.application_init(args)
528 view = worktree_browser(context, show=False, update=False)
529 return app.application_start(context, view)
532 def cmd_clone(args):
533 from .widgets import clone
535 context = app.application_init(args)
536 view = clone.clone(context)
537 context.set_view(view)
538 result = 0 if view.exec_() == view.Accepted else 1
539 app.default_stop(context, view)
540 return result
543 def cmd_config(args):
544 from .widgets.prefs import preferences
546 context = app.application_init(args)
547 view = preferences(context)
548 return app.application_start(context, view)
551 def cmd_dag(args):
552 from .widgets import dag
554 context = app.application_init(args)
555 # cola.main() uses parse_args(), unlike dag.main() which uses
556 # parse_known_args(), thus we aren't able to automatically forward
557 # all unknown arguments. Special-case support for "--all" since it's
558 # used by the history viewer command on Windows.
559 if args.show_all:
560 args.args.insert(0, '--all')
561 view = dag.git_dag(context, args=args, show=False)
562 return app.application_start(context, view)
565 def cmd_diff(args):
566 from .difftool import diff_expression
568 context = app.application_init(args)
569 expr = core.list2cmdline(args.args)
570 view = diff_expression(context, None, expr, create_widget=True)
571 return app.application_start(context, view)
574 def cmd_fetch(args):
575 # TODO: the calls to update_status() can be done asynchronously
576 # by hooking into the message_updated notification.
577 from .widgets import remote
579 context = app.application_init(args)
580 context.model.update_status()
581 view = remote.fetch(context)
582 return app.application_start(context, view)
585 def cmd_find(args):
586 from .widgets import finder
588 context = app.application_init(args)
589 paths = core.list2cmdline(args.paths)
590 view = finder.finder(context, paths=paths)
591 return app.application_start(context, view)
594 def cmd_grep(args):
595 from .widgets import grep
597 context = app.application_init(args)
598 text = core.list2cmdline(args.args)
599 view = grep.new_grep(context, text=text, parent=None)
600 return app.application_start(context, view)
603 def cmd_merge(args):
604 from .widgets.merge import Merge
606 context = app.application_init(args, update=True)
607 view = Merge(context, parent=None, ref=args.ref)
608 return app.application_start(context, view)
611 def cmd_version(args):
612 from . import version
614 version.print_version(builtin=args.builtin, brief=args.brief)
615 return 0
618 def cmd_pull(args):
619 from .widgets import remote
621 context = app.application_init(args, update=True)
622 view = remote.pull(context)
623 if args.rebase:
624 view.set_rebase(True)
625 return app.application_start(context, view)
628 def cmd_push(args):
629 from .widgets import remote
631 context = app.application_init(args, update=True)
632 view = remote.push(context)
633 return app.application_start(context, view)
636 def cmd_rebase(args):
637 kwargs = {
638 'verbose': args.verbose,
639 'quiet': args.quiet,
640 'autostash': args.autostash,
641 'fork_point': args.fork_point,
642 'onto': args.onto,
643 'preserve_merges': args.preserve_merges,
644 'strategy': args.strategy,
645 'no_ff': args.no_ff,
646 'merge': args.merge,
647 'exec': getattr(args, 'exec', None), # python keyword
648 'keep_empty': args.keep_empty,
649 'force_rebase': args.force_rebase,
650 'strategy_option': args.strategy_option,
651 'stat': args.stat,
652 'no_stat': args.no_stat,
653 'verify': args.verify,
654 'rerere_autoupdate': args.rerere_autoupdate,
655 'root': args.root,
656 'autosquash': args.autosquash,
657 'committer_date_is_author_date': args.committer_date_is_author_date,
658 'ignore_date': args.ignore_date,
659 'whitespace': args.whitespace,
660 'ignore_whitespace': args.ignore_whitespace,
661 'C': args.context_lines,
662 'continue': getattr(args, 'continue', False), # python keyword
663 'abort': args.abort,
664 'skip': args.skip,
665 'edit_todo': args.edit_todo,
666 'update_refs': args.update_refs,
667 'upstream': args.upstream,
668 'branch': args.branch,
670 context = app.application_init(args)
671 status, _, _ = cmds.do(cmds.Rebase, context, **kwargs)
672 return status
675 def cmd_recent(args):
676 from .widgets import recent
678 context = app.application_init(args)
679 view = recent.browse_recent_files(context)
680 return app.application_start(context, view)
683 def cmd_remote(args):
684 from .widgets import editremotes
686 context = app.application_init(args)
687 view = editremotes.editor(context, run=False)
688 return app.application_start(context, view)
691 def cmd_search(args):
692 from .widgets.search import search
694 context = app.application_init(args)
695 view = search(context)
696 return app.application_start(context, view)
699 def cmd_stash(args):
700 from .widgets import stash
702 context = app.application_init(args)
703 view = stash.view(context, show=False)
704 return app.application_start(context, view)
707 def cmd_tag(args):
708 from .widgets.createtag import new_create_tag
710 context = app.application_init(args)
711 context.model.update_status()
712 view = new_create_tag(context, name=args.name, ref=args.ref, sign=args.sign)
713 return app.application_start(context, view)
716 # Windows shortcut launch features:
717 def shortcut_launch():
718 """Launch from a shortcut
720 Prompt for the repository by default.
723 argv = sys.argv[1:]
724 if not argv:
725 argv = ['cola', '--prompt']
726 return main(argv=argv)