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
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
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 */
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)
49 sip
.setapi('QVariant', 1)
52 from PyQt4
import QtCore
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
64 from cola
import compat
66 from cola
import gitcfg
67 from cola
import icons
68 from cola
import inotify
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
)
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', '')
112 git_askpass
= core
.getenv('GIT_ASKPASS')
113 ssh_askpass
= core
.getenv('SSH_ASKPASS')
115 askpass
= git_askpass
117 askpass
= ssh_askpass
118 elif sys
.platform
== 'darwin':
119 askpass
= resources
.share('bin', 'ssh-askpass-darwin')
121 askpass
= resources
.share('bin', 'ssh-askpass')
123 compat
.setenv('GIT_ASKPASS', askpass
)
124 compat
.setenv('SSH_ASKPASS', askpass
)
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
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.
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):
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
)
180 self
._app
= current(tuple(argv
))
181 self
._app
.setWindowIcon(icons
.cola())
183 self
._app
= QtCore
.QCoreApplication(argv
)
185 def activeWindow(self
):
186 """Wrap activeWindow()"""
187 return self
._app
.activeWindow()
190 return self
._app
.desktop()
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()'))
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()
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:
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
):
239 # Accept 'git cola --version' or 'git cola version'
240 version
.print_version()
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
257 # We do everything relative to the repo root
262 def restore_session(args
):
263 # args.settings is provided when restoring from a session.
265 if args
.session
is None:
267 session
= Session(args
.session
)
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.
281 app
= new_application(args
)
282 model
= new_model(app
, args
.repo
,
283 prompt
=args
.prompt
, settings
=args
.settings
)
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
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
306 msg_timer
= QtCore
.QTimer()
307 msg_timer
.setSingleShot(True)
308 msg_timer
.connect(msg_timer
, SIGNAL('timeout()'), _send_msg
)
311 # Start the event loop
312 result
= context
.app
.exec_()
316 QtCore
.QThreadPool
.globalInstance().waitForDone()
318 tmpdir
= utils
.tmpdir()
319 shutil
.rmtree(tmpdir
, ignore_errors
=True)
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
):
344 return ColaApplication(sys
.argv
)
347 def new_model(app
, repo
, prompt
=False, settings
=None):
349 valid
= model
.set_worktree(repo
) and not prompt
351 startup_dlg
= startup
.StartupDialog(app
.activeWindow(),
353 gitdir
= startup_dlg
.find_git_repo()
356 valid
= model
.set_worktree(gitdir
)
358 # Finally, go to the root of the git repo
359 os
.chdir(model
.git
.worktree())
363 def init_update_task(parent
, runtask
, model
):
364 """Update the model in the background
366 git-cola should startup as quickly as possible.
371 model
.update_status(update_index
=True)
373 task
= qtutils
.SimpleTask(parent
, update_status
)
378 if git
.GIT_COLA_TRACE
== 'trace':
379 msg
= 'info: debug mode enabled using GIT_COLA_TRACE=trace'
383 class ApplicationContext(object):
385 def __init__(self
, args
, app
, cfg
, model
):