cmds: provide $DIRNAME in the environment for guitool commands
[git-cola.git] / cola / main.py
blob65847e67c64deeb46d67835608ccb5e55b8d3bd7
1 """Launcher and command line interface to git-cola"""
3 from __future__ import absolute_import, division, unicode_literals
5 import argparse
6 import os
7 import sys
9 from .app import add_common_arguments
10 from .app import application_init
11 from .app import application_start
13 # NOTE: these must be imported *after* cola.app.
14 # PyQt4 may not be available until after cola.app has gotten a chance to
15 # install the homebrew modules in sys.path.
16 from . import cmds
17 from . import compat
18 from . import core
19 from . import utils
22 def main(argv=None):
23 if argv is None:
24 argv = sys.argv[1:]
25 # we're using argparse with subparser, but argparse
26 # does not allow us to assign a default subparser
27 # when none has been specified. We fake it by injecting
28 # 'cola' into the command-line so that parse_args()
29 # routes them to the 'cola' parser by default.
30 if (len(argv) < 1 or
31 argv[0].startswith('-') and
32 '--help-commands' not in argv):
33 argv.insert(0, 'cola')
34 elif '--help-commands' in argv:
35 argv.append('--help')
36 args = parse_args(argv)
37 return args.func(args)
40 def parse_args(argv):
41 parser = argparse.ArgumentParser()
42 subparser = parser.add_subparsers(title='valid commands')
44 add_cola_command(subparser)
45 add_about_command(subparser)
46 add_am_command(subparser)
47 add_archive_command(subparser)
48 add_branch_command(subparser)
49 add_browse_command(subparser)
50 add_config_command(subparser)
51 add_dag_command(subparser)
52 add_diff_command(subparser)
53 add_fetch_command(subparser)
54 add_find_command(subparser)
55 add_grep_command(subparser)
56 add_merge_command(subparser)
57 add_pull_command(subparser)
58 add_push_command(subparser)
59 add_rebase_command(subparser)
60 add_remote_command(subparser)
61 add_search_command(subparser)
62 add_stash_command(subparser)
63 add_tag_command(subparser)
64 add_version_command(subparser)
66 return parser.parse_args(argv)
69 def add_command(parent, name, description, func):
70 parser = parent.add_parser(str(name), help=description)
71 parser.set_defaults(func=func)
72 add_common_arguments(parser)
73 return parser
76 def add_cola_command(subparser):
77 parser = add_command(subparser, 'cola', 'start git-cola', cmd_cola)
78 parser.add_argument('--amend', default=False, action='store_true',
79 help='start in amend mode')
80 parser.add_argument('--help-commands', default=False, action='store_true',
81 help='show available sub-commands')
82 parser.add_argument('--status-filter', '-s', metavar='<path>',
83 default='', help='status path filter')
86 def add_about_command(parent):
87 parser = add_command(parent, 'about', 'about git-cola', cmd_about)
89 def add_am_command(parent):
90 parser = add_command(parent, 'am', 'apply patches using "git am"', cmd_am)
91 parser.add_argument('patches', metavar='<patches>', nargs='*',
92 help='patches to apply')
95 def add_archive_command(parent):
96 parser = add_command(parent, 'archive', 'save an archive', cmd_archive)
97 parser.add_argument('ref', metavar='<ref>', nargs='?', default=None,
98 help='commit to archive')
101 def add_branch_command(subparser):
102 add_command(subparser, 'branch', 'create a branch', cmd_branch)
105 def add_browse_command(subparser):
106 add_command(subparser, 'browse', 'browse repository', cmd_browse)
107 add_command(subparser, 'classic', 'browse repository', cmd_browse)
110 def add_config_command(subparser):
111 add_command(subparser, 'config', 'edit configuration', cmd_config)
114 def add_dag_command(subparser):
115 parser = add_command(subparser, 'dag', 'start git-dag', cmd_dag)
116 parser.add_argument('-c', '--count', metavar='<count>',
117 type=int, default=1000,
118 help='number of commits to display')
119 parser.add_argument('args', nargs='*', metavar='<args>',
120 help='git log arguments')
123 def add_diff_command(subparser):
124 parser = add_command(subparser, 'diff', 'view diffs', cmd_diff)
125 parser.add_argument('args', nargs='*', metavar='<args>',
126 help='git diff arguments')
129 def add_fetch_command(subparser):
130 add_command(subparser, 'fetch', 'fetch remotes', cmd_fetch)
133 def add_find_command(subparser):
134 parser = add_command(subparser, 'find', 'find files', cmd_find)
135 parser.add_argument('paths', nargs='*', metavar='<path>',
136 help='filter by path')
139 def add_grep_command(subparser):
140 parser = add_command(subparser, 'grep', 'grep source', cmd_grep)
141 parser.add_argument('args', nargs='*', metavar='<args>',
142 help='git grep arguments')
145 def add_merge_command(subparser):
146 add_command(subparser, 'merge', 'merge branches', cmd_merge)
149 def add_pull_command(subparser):
150 parser = add_command(subparser, 'pull', 'pull remote branches', cmd_pull)
151 parser.add_argument('--rebase', default=False, action='store_true',
152 help='rebase local branch when pulling')
155 def add_push_command(subparser):
156 add_command(subparser, 'push', 'push remote branches', cmd_push)
159 def add_rebase_command(subparser):
160 parser = add_command(subparser, 'rebase', 'interactive rebase', cmd_rebase)
161 parser.add_argument('-v', '--verbose', default=False, action='store_true',
162 help='display a diffstat of what changed upstream')
163 parser.add_argument('-q', '--quiet', default=False, action='store_true',
164 help='be quiet. implies --no-stat')
165 parser.add_argument('-i', '--interactive', default=True,
166 action='store_true', help=argparse.SUPPRESS)
167 parser.add_argument('--autostash', default=False, action='store_true',
168 help='automatically stash/stash pop before and after')
169 parser.add_argument('--fork-point', default=False, action='store_true',
170 help="use 'merge-base --fork-point' to refine upstream")
171 parser.add_argument('--onto', default=None, metavar='<newbase>',
172 help='rebase onto given branch instead of upstream')
173 parser.add_argument('-p', '--preserve-merges',
174 default=False, action='store_true',
175 help='try to recreate merges instead of ignoring them')
176 parser.add_argument('-s', '--strategy', default=None, metavar='<strategy>',
177 help='use the given merge strategy')
178 parser.add_argument('--no-ff', default=False, action='store_true',
179 help='cherry-pick all commits, even if unchanged')
180 parser.add_argument('-m', '--merge', default=False, action='store_true',
181 help='use merging strategies to rebase')
182 parser.add_argument('-x', '--exec', default=None,
183 help='add exec lines after each commit of '
184 'the editable list')
185 parser.add_argument('-k', '--keep-empty', default=False,
186 action='store_true',
187 help='preserve empty commits during rebase')
188 parser.add_argument('-f', '--force-rebase', default=False,
189 action='store_true',
190 help='force rebase even if branch is up to date')
191 parser.add_argument('-X', '--strategy-option', default=None,
192 metavar='<arg>',
193 help='pass the argument through to the merge strategy')
194 parser.add_argument('--stat', default=False, action='store_true',
195 help='display a diffstat of what changed upstream')
196 parser.add_argument('-n', '--no-stat', default=False, action='store_true',
197 help='do not show diffstat of what changed upstream')
198 parser.add_argument('--verify', default=False, action='store_true',
199 help='allow pre-rebase hook to run')
200 parser.add_argument('--rerere-autoupdate',
201 default=False, action='store_true',
202 help='allow rerere to update index with '
203 'resolved conflicts')
204 parser.add_argument('--root', default=False, action='store_true',
205 help='rebase all reachable commits up to the root(s)')
206 parser.add_argument('--autosquash', default=True, action='store_true',
207 help='move commits that begin with '
208 'squash!/fixup! under -i')
209 parser.add_argument('--no-autosquash', default=True, action='store_false',
210 dest='autosquash',
211 help='do not move commits that begin with '
212 'squash!/fixup! under -i')
213 parser.add_argument('--committer-date-is-author-date',
214 default=False, action='store_true',
215 help="passed to 'git am' by 'git rebase'")
216 parser.add_argument('--ignore-date', default=False, action='store_true',
217 help="passed to 'git am' by 'git rebase'")
218 parser.add_argument('--whitespace', default=False, action='store_true',
219 help="passed to 'git apply' by 'git rebase'")
220 parser.add_argument('--ignore-whitespace', default=False,
221 action='store_true',
222 help="passed to 'git apply' by 'git rebase'")
223 parser.add_argument('-C', dest='context_lines', default=None, metavar='<n>',
224 help="passed to 'git apply' by 'git rebase'")
226 actions = parser.add_argument_group('actions')
227 actions.add_argument('--continue', default=False, action='store_true',
228 help='continue')
229 actions.add_argument('--abort', default=False, action='store_true',
230 help='abort and check out the original branch')
231 actions.add_argument('--skip', default=False, action='store_true',
232 help='skip current patch and continue')
233 actions.add_argument('--edit-todo', default=False, action='store_true',
234 help='edit the todo list during an interactive rebase')
236 parser.add_argument('upstream', nargs='?', default=None,
237 metavar='<upstream>',
238 help='the upstream configured in branch.<name>.remote '
239 'and branch.<name>.merge options will be used '
240 'when <upstream> is omitted; see git-rebase(1) '
241 'for details. If you are currently not on any '
242 'branch or if the current branch does not have '
243 'a configured upstream, the rebase will abort')
244 parser.add_argument('branch', nargs='?', default=None, metavar='<branch>',
245 help='git rebase will perform an automatic '
246 '"git checkout <branch>" before doing anything '
247 'else when <branch> is specified')
250 def add_remote_command(subparser):
251 add_command(subparser, 'remote', 'edit remotes', cmd_remote)
254 def add_search_command(subparser):
255 add_command(subparser, 'search', 'search commits', cmd_search)
258 def add_stash_command(subparser):
259 add_command(subparser, 'stash', 'stash and unstash changes', cmd_stash)
262 def add_tag_command(subparser):
263 parser = add_command(subparser, 'tag', 'create tags', cmd_tag)
264 parser.add_argument('name', metavar='<name>', nargs='?', default=None,
265 help='tag name')
266 parser.add_argument('ref', metavar='<ref>', nargs='?', default=None,
267 help='commit to tag')
268 parser.add_argument('-s', '--sign', default=False, action='store_true',
269 help='annotated and GPG-signed tag')
272 def add_version_command(subparser):
273 parser = add_command(subparser, 'version', 'print the version', cmd_version)
274 parser.add_argument('--brief', action='store_true', default=False,
275 help='print the version number only')
278 # entry points
279 def cmd_cola(args):
280 from .widgets.main import MainView
281 status_filter = args.status_filter
282 if status_filter:
283 status_filter = core.abspath(status_filter)
285 context = application_init(args)
286 view = MainView(context.model, settings=args.settings)
287 if args.amend:
288 cmds.do(cmds.AmendMode, True)
290 if status_filter:
291 view.set_filter(core.relpath(status_filter))
293 return application_start(context, view)
296 def cmd_about(args):
297 from .widgets import about
298 context = application_init(args)
299 view = about.about_dialog()
300 return application_start(context, view)
303 def cmd_am(args):
304 from .widgets.patch import new_apply_patches
305 context = application_init(args)
306 view = new_apply_patches(patches=args.patches)
307 return application_start(context, view)
310 def cmd_archive(args):
311 from .widgets.archive import GitArchiveDialog
312 context = application_init(args, update=True)
313 if args.ref is None:
314 args.ref = context.model.currentbranch
315 view = GitArchiveDialog(args.ref)
316 return application_start(context, view)
319 def cmd_branch(args):
320 from .widgets.createbranch import create_new_branch
321 context = application_init(args, update=True)
322 view = create_new_branch(settings=args.settings)
323 return application_start(context, view)
326 def cmd_browse(args):
327 from .widgets.browse import worktree_browser
328 context = application_init(args)
329 view = worktree_browser(update=False, settings=args.settings)
330 return application_start(context, view)
333 def cmd_config(args):
334 from .widgets.prefs import preferences
335 context = application_init(args)
336 view = preferences()
337 return application_start(context, view)
340 def cmd_dag(args):
341 context = application_init(args)
342 from .widgets.dag import git_dag
343 view = git_dag(context.model, args=args, settings=args.settings)
344 return application_start(context, view)
347 def cmd_diff(args):
348 context = application_init(args)
349 from .difftool import diff_expression
350 expr = core.list2cmdline(args.args)
351 view = diff_expression(None, expr, create_widget=True)
352 return application_start(context, view)
355 def cmd_fetch(args):
356 # TODO: the calls to update_status() can be done asynchronously
357 # by hooking into the message_updated notification.
358 context = application_init(args)
359 from .widgets import remote
360 context.model.update_status()
361 view = remote.fetch()
362 return application_start(context, view)
365 def cmd_find(args):
366 context = application_init(args)
367 from .widgets import finder
368 paths = core.list2cmdline(args.paths)
369 view = finder.finder(paths=paths)
370 return application_start(context, view)
373 def cmd_grep(args):
374 context = application_init(args)
375 from .widgets import grep
376 text = core.list2cmdline(args.args)
377 view = grep.new_grep(text=text, parent=None)
378 return application_start(context, view)
381 def cmd_merge(args):
382 context = application_init(args, update=True)
383 from .widgets.merge import MergeView
384 view = MergeView(context.cfg, context.model, parent=None)
385 return application_start(context, view)
388 def cmd_version(args):
389 from . import version
390 version.print_version(brief=args.brief)
391 return 0
394 def cmd_pull(args):
395 from .widgets import remote
396 context = application_init(args, update=True)
397 view = remote.pull()
398 if args.rebase:
399 view.set_rebase(True)
400 return application_start(context, view)
403 def cmd_push(args):
404 from .widgets import remote
405 context = application_init(args, update=True)
406 view = remote.push()
407 return application_start(context, view)
410 def cmd_rebase(args):
411 kwargs = {
412 'verbose': args.verbose,
413 'quiet': args.quiet,
414 'autostash': args.autostash,
415 'fork_point': args.fork_point,
416 'onto': args.onto,
417 'preserve_merges': args.preserve_merges,
418 'strategy': args.strategy,
419 'no_ff': args.no_ff,
420 'merge': args.merge,
421 'exec': getattr(args, 'exec', None), # python keyword
422 'keep_empty': args.keep_empty,
423 'force_rebase': args.force_rebase,
424 'strategy_option': args.strategy_option,
425 'stat': args.stat,
426 'no_stat': args.no_stat,
427 'verify': args.verify,
428 'rerere_autoupdate': args.rerere_autoupdate,
429 'root': args.root,
430 'autosquash': args.autosquash,
431 'committer_date_is_author_date': args.committer_date_is_author_date,
432 'ignore_date': args.ignore_date,
433 'whitespace': args.whitespace,
434 'ignore_whitespace': args.ignore_whitespace,
435 'C': args.context_lines,
436 'continue': getattr(args, 'continue', False), # python keyword
437 'abort': args.abort,
438 'skip': args.skip,
439 'edit_todo': args.edit_todo,
440 'upstream': args.upstream,
441 'branch': args.branch,
442 'capture_output': False,
444 status, out, err = cmds.do(cmds.Rebase, **kwargs)
445 if out:
446 core.stdout(out)
447 if err:
448 core.stderr(err)
449 return status
452 def cmd_remote(args):
453 from .widgets import editremotes
454 context = application_init(args)
455 view = editremotes.new_remote_editor()
456 return application_start(context, view)
459 def cmd_search(args):
460 from .widgets.search import search
461 context = application_init(args)
462 view = search()
463 return application_start(context, view)
466 def cmd_stash(args):
467 from .widgets.stash import stash
468 context = application_init(args)
469 view = stash()
470 return application_start(context, view)
473 def cmd_tag(args):
474 from .widgets.createtag import new_create_tag
475 context = application_init(args)
476 view = new_create_tag(name=args.name, ref=args.ref, sign=args.sign,
477 settings=args.settings)
478 return application_start(context, view)
481 # Windows shortcut launch features:
483 def find_git():
484 """Return the path of git.exe, or None if we can't find it."""
485 if not utils.is_win32():
486 return None # UNIX systems have git in their $PATH
488 # If the user wants to use a Git/bin/ directory from a non-standard
489 # directory then they can write its location into
490 # ~/.config/git-cola/git-bindir
491 git_bindir = os.path.expanduser(os.path.join('~', '.config', 'git-cola',
492 'git-bindir'))
493 if core.exists(git_bindir):
494 custom_path = core.read(git_bindir).strip()
495 if custom_path and core.exists(custom_path):
496 return custom_path
498 # Try to find Git's bin/ directory in one of the typical locations
499 pf = os.environ.get('ProgramFiles', 'C:\\Program Files')
500 pf32 = os.environ.get('ProgramFiles(x86)', 'C:\\Program Files (x86)')
501 for p in [pf32, pf, 'C:\\']:
502 candidate = os.path.join(p, 'Git\\bin')
503 if os.path.isdir(candidate):
504 return candidate
506 return None
509 def shortcut_launch():
510 """Launch from a shortcut
512 Prompt for the repository by default, and try to find git.
514 argv = ['cola', '--prompt']
515 git_path = find_git()
516 if git_path:
517 prepend_path(git_path)
519 return main(argv)
522 def prepend_path(path):
523 # Adds git to the PATH. This is needed on Windows.
524 path = core.decode(path)
525 path_entries = core.getenv('PATH', '').split(os.pathsep)
526 if path not in path_entries:
527 path_entries.insert(0, path)
528 compat.setenv('PATH', os.pathsep.join(path_entries))