qtutils: avoid None in add_items()
[git-cola.git] / cola / app.py
blob4b72f73d60158571c7693a1e6f0431e3a723be4d
1 # Copyright (C) 2009, 2010, 2011, 2012, 2013
2 # David Aguilar <davvid@gmail.com>
3 """Provides the main() routine and ColaApplication"""
4 from __future__ import division, absolute_import, unicode_literals
6 import argparse
7 import os
8 import shutil
9 import signal
10 import sys
12 # Make homebrew work by default
13 if sys.platform == 'darwin':
14 from distutils import sysconfig
15 python_version = sysconfig.get_python_version()
16 homebrew_mods = '/usr/local/lib/python%s/site-packages' % python_version
17 if os.path.isdir(homebrew_mods):
18 sys.path.append(homebrew_mods)
21 errmsg = """Sorry, you do not seem to have PyQt4 installed.
22 Please install it before using git-cola.
23 e.g.: sudo apt-get install python-qt4
24 """
26 # /usr/include/sysexits.h
27 #define EX_OK 0 /* successful termination */
28 #define EX_USAGE 64 /* command line usage error */
29 #define EX_NOINPUT 66 /* cannot open input */
30 #define EX_UNAVAILABLE 69 /* service unavailable */
31 EX_OK = 0
32 EX_USAGE = 64
33 EX_NOINPUT = 66
34 EX_UNAVAILABLE = 69
37 try:
38 import sip
39 except ImportError:
40 sys.stderr.write(errmsg)
41 sys.exit(EX_UNAVAILABLE)
43 sip.setapi('QString', 1)
44 sip.setapi('QDate', 1)
45 sip.setapi('QDateTime', 1)
46 sip.setapi('QTextStream', 1)
47 sip.setapi('QTime', 1)
48 sip.setapi('QUrl', 1)
49 sip.setapi('QVariant', 1)
51 try:
52 from PyQt4 import QtCore
53 except ImportError:
54 sys.stderr.write(errmsg)
55 sys.exit(EX_UNAVAILABLE)
57 from PyQt4 import QtGui
58 from PyQt4.QtCore import Qt
59 from PyQt4.QtCore import SIGNAL
61 # Import cola modules
62 from cola import cmds
63 from cola import core
64 from cola import compat
65 from cola import git
66 from cola import gitcfg
67 from cola import icons
68 from cola import inotify
69 from cola import i18n
70 from cola import qtcompat
71 from cola import qtutils
72 from cola import resources
73 from cola import utils
74 from cola import version
75 from cola.compat import ustr
76 from cola.decorators import memoize
77 from cola.i18n import N_
78 from cola.interaction import Interaction
79 from cola.models import main
80 from cola.widgets import cfgactions
81 from cola.widgets import startup
82 from cola.settings import Session
85 def setup_environment():
86 # Allow Ctrl-C to exit
87 signal.signal(signal.SIGINT, signal.SIG_DFL)
89 # Session management wants an absolute path when restarting
90 sys.argv[0] = sys_argv0 = core.abspath(sys.argv[0])
92 # Spoof an X11 display for SSH
93 os.environ.setdefault('DISPLAY', ':0')
95 if not core.getenv('SHELL', ''):
96 for shell in ('/bin/zsh', '/bin/bash', '/bin/sh'):
97 if os.path.exists(shell):
98 compat.setenv('SHELL', shell)
99 break
101 # Setup the path so that git finds us when we run 'git cola'
102 path_entries = core.getenv('PATH', '').split(os.pathsep)
103 bindir = os.path.dirname(sys_argv0)
104 path_entries.append(bindir)
105 path = os.pathsep.join(path_entries)
106 compat.setenv('PATH', path)
108 # We don't ever want a pager
109 compat.setenv('GIT_PAGER', '')
111 # Setup *SSH_ASKPASS
112 git_askpass = core.getenv('GIT_ASKPASS')
113 ssh_askpass = core.getenv('SSH_ASKPASS')
114 if git_askpass:
115 askpass = git_askpass
116 elif ssh_askpass:
117 askpass = ssh_askpass
118 elif sys.platform == 'darwin':
119 askpass = resources.share('bin', 'ssh-askpass-darwin')
120 else:
121 askpass = resources.share('bin', 'ssh-askpass')
123 compat.setenv('GIT_ASKPASS', askpass)
124 compat.setenv('SSH_ASKPASS', askpass)
126 # --- >8 --- >8 ---
127 # Git v1.7.10 Release Notes
128 # =========================
130 # Compatibility Notes
131 # -------------------
133 # * From this release on, the "git merge" command in an interactive
134 # session will start an editor when it automatically resolves the
135 # merge for the user to explain the resulting commit, just like the
136 # "git commit" command does when it wasn't given a commit message.
138 # If you have a script that runs "git merge" and keeps its standard
139 # input and output attached to the user's terminal, and if you do not
140 # want the user to explain the resulting merge commits, you can
141 # export GIT_MERGE_AUTOEDIT environment variable set to "no", like
142 # this:
144 # #!/bin/sh
145 # GIT_MERGE_AUTOEDIT=no
146 # export GIT_MERGE_AUTOEDIT
148 # to disable this behavior (if you want your users to explain their
149 # merge commits, you do not have to do anything). Alternatively, you
150 # can give the "--no-edit" option to individual invocations of the
151 # "git merge" command if you know everybody who uses your script has
152 # Git v1.7.8 or newer.
153 # --- >8 --- >8 ---
154 # Longer-term: Use `git merge --no-commit` so that we always
155 # have a chance to explain our merges.
156 compat.setenv('GIT_MERGE_AUTOEDIT', 'no')
159 # style note: we use camelCase here since we're masquerading a Qt class
160 class ColaApplication(object):
161 """The main cola application
163 ColaApplication handles i18n of user-visible data
166 def __init__(self, argv, locale=None, gui=True):
167 cfgactions.install()
168 i18n.install(locale)
169 qtcompat.install()
170 qtutils.install()
171 icons.install()
173 self.notifier = QtCore.QObject()
174 self.notifier.connect(self.notifier, SIGNAL('update_files()'),
175 self._update_files, Qt.QueuedConnection)
176 # Call _update_files when inotify detects changes
177 inotify.observer(self._update_files_notifier)
179 if gui:
180 self._app = current(tuple(argv))
181 self._app.setWindowIcon(icons.cola())
182 else:
183 self._app = QtCore.QCoreApplication(argv)
185 def activeWindow(self):
186 """Wrap activeWindow()"""
187 return self._app.activeWindow()
189 def desktop(self):
190 return self._app.desktop()
192 def exec_(self):
193 """Wrap exec_()"""
194 return self._app.exec_()
196 def set_view(self, view):
197 if hasattr(self._app, 'view'):
198 self._app.view = view
200 def _update_files(self):
201 # Respond to inotify updates
202 cmds.do(cmds.Refresh)
204 def _update_files_notifier(self):
205 self.notifier.emit(SIGNAL('update_files()'))
208 @memoize
209 def current(argv):
210 return ColaQApplication(list(argv))
213 class ColaQApplication(QtGui.QApplication):
215 def __init__(self, argv):
216 QtGui.QApplication.__init__(self, argv)
217 self.view = None ## injected by application_start()
219 def event(self, e):
220 if e.type() == QtCore.QEvent.ApplicationActivate:
221 cfg = gitcfg.current()
222 if cfg.get('cola.refreshonfocus', False):
223 cmds.do(cmds.Refresh)
224 return QtGui.QApplication.event(self, e)
226 def commitData(self, session_mgr):
227 """Save session data"""
228 if self.view is None:
229 return
230 sid = ustr(session_mgr.sessionId())
231 skey = ustr(session_mgr.sessionKey())
232 session_id = '%s_%s' % (sid, skey)
233 session = Session(session_id, repo=core.getcwd())
234 self.view.save_state(settings=session)
237 def process_args(args):
238 if args.version:
239 # Accept 'git cola --version' or 'git cola version'
240 version.print_version()
241 sys.exit(EX_OK)
243 # Handle session management
244 restore_session(args)
246 # Bail out if --repo is not a directory
247 repo = core.decode(args.repo)
248 if repo.startswith('file:'):
249 repo = repo[len('file:'):]
250 repo = core.realpath(repo)
251 if not core.isdir(repo):
252 errmsg = N_('fatal: "%s" is not a directory. '
253 'Please specify a correct --repo <path>.') % repo
254 core.stderr(errmsg)
255 sys.exit(EX_USAGE)
257 # We do everything relative to the repo root
258 os.chdir(args.repo)
259 return repo
262 def restore_session(args):
263 # args.settings is provided when restoring from a session.
264 args.settings = None
265 if args.session is None:
266 return
267 session = Session(args.session)
268 if session.load():
269 args.settings = session
270 args.repo = session.repo
273 def application_init(args, update=False):
274 """Parses the command-line arguments and starts git-cola
276 # Ensure that we're working in a valid git repository.
277 # If not, try to find one. When found, chdir there.
278 setup_environment()
279 process_args(args)
281 app = new_application(args)
282 model = new_model(app, args.repo,
283 prompt=args.prompt, settings=args.settings)
284 if update:
285 model.update_status()
286 cfg = gitcfg.current()
287 return ApplicationContext(args, app, cfg, model)
290 def application_start(context, view):
291 """Show the GUI and start the main event loop"""
292 # Store the view for session management
293 context.app.set_view(view)
295 # Make sure that we start out on top
296 view.show()
297 view.raise_()
299 # Scan for the first time
300 runtask = qtutils.RunTask(parent=view)
301 init_update_task(view, runtask, context.model)
303 # Start the inotify thread
304 inotify.start()
306 msg_timer = QtCore.QTimer()
307 msg_timer.setSingleShot(True)
308 msg_timer.connect(msg_timer, SIGNAL('timeout()'), _send_msg)
309 msg_timer.start(0)
311 # Start the event loop
312 result = context.app.exec_()
314 # All done, cleanup
315 inotify.stop()
316 QtCore.QThreadPool.globalInstance().waitForDone()
318 tmpdir = utils.tmpdir()
319 shutil.rmtree(tmpdir, ignore_errors=True)
321 return result
324 def add_common_arguments(parser):
325 # We also accept 'git cola version'
326 parser.add_argument('--version', default=False, action='store_true',
327 help='print version number')
329 # Specifies a git repository to open
330 parser.add_argument('-r', '--repo', metavar='<repo>', default=core.getcwd(),
331 help='open the specified git repository')
333 # Specifies that we should prompt for a repository at startup
334 parser.add_argument('--prompt', action='store_true', default=False,
335 help='prompt for a repository')
337 # Resume an X Session Management session
338 parser.add_argument('-session', metavar='<session>', default=None,
339 help=argparse.SUPPRESS)
342 def new_application(args):
343 # Initialize the app
344 return ColaApplication(sys.argv)
347 def new_model(app, repo, prompt=False, settings=None):
348 model = main.model()
349 valid = model.set_worktree(repo) and not prompt
350 while not valid:
351 startup_dlg = startup.StartupDialog(app.activeWindow(),
352 settings=settings)
353 gitdir = startup_dlg.find_git_repo()
354 if not gitdir:
355 sys.exit(EX_NOINPUT)
356 valid = model.set_worktree(gitdir)
358 # Finally, go to the root of the git repo
359 os.chdir(model.git.worktree())
360 return model
363 def init_update_task(parent, runtask, model):
364 """Update the model in the background
366 git-cola should startup as quickly as possible.
370 def update_status():
371 model.update_status(update_index=True)
373 task = qtutils.SimpleTask(parent, update_status)
374 runtask.start(task)
377 def _send_msg():
378 if git.GIT_COLA_TRACE == 'trace':
379 msg = 'info: debug mode enabled using GIT_COLA_TRACE=trace'
380 Interaction.log(msg)
383 class ApplicationContext(object):
385 def __init__(self, args, app, cfg, model):
386 self.args = args
387 self.app = app
388 self.cfg = cfg
389 self.model = model