1 """Provides the main() routine and ColaApplication"""
2 from __future__
import division
, absolute_import
, unicode_literals
9 Copyright (C) 2009-2016 David Aguilar and contributors
14 from qtpy
import QtCore
17 Sorry, you do not seem to have PyQt5, Pyside, or PyQt4 installed.
18 Please install it before using git-cola, e.g.:
19 $ sudo apt-get install python-qt4
23 from qtpy
import QtGui
24 from qtpy
import QtWidgets
27 from .decorators
import memoize
29 from .interaction
import Interaction
30 from .models
import main
31 from .widgets
import cfgactions
32 from .widgets
import defs
33 from .widgets
import startup
34 from .settings
import Session
38 from . import fsmonitor
43 from . import qtcompat
45 from . import resources
49 def setup_environment():
50 # Allow Ctrl-C to exit
51 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
53 # Session management wants an absolute path when restarting
54 sys
.argv
[0] = sys_argv0
= os
.path
.abspath(sys
.argv
[0])
56 # Spoof an X11 display for SSH
57 os
.environ
.setdefault('DISPLAY', ':0')
59 if not core
.getenv('SHELL', ''):
60 for shell
in ('/bin/zsh', '/bin/bash', '/bin/sh'):
61 if os
.path
.exists(shell
):
62 compat
.setenv('SHELL', shell
)
65 # Setup the path so that git finds us when we run 'git cola'
66 path_entries
= core
.getenv('PATH', '').split(os
.pathsep
)
67 bindir
= core
.decode(os
.path
.dirname(sys_argv0
))
68 path_entries
.append(bindir
)
69 path
= os
.pathsep
.join(path_entries
)
70 compat
.setenv('PATH', path
)
72 # We don't ever want a pager
73 compat
.setenv('GIT_PAGER', '')
76 git_askpass
= core
.getenv('GIT_ASKPASS')
77 ssh_askpass
= core
.getenv('SSH_ASKPASS')
82 elif sys
.platform
== 'darwin':
83 askpass
= resources
.share('bin', 'ssh-askpass-darwin')
85 askpass
= resources
.share('bin', 'ssh-askpass')
87 compat
.setenv('GIT_ASKPASS', askpass
)
88 compat
.setenv('SSH_ASKPASS', askpass
)
91 # Git v1.7.10 Release Notes
92 # =========================
97 # * From this release on, the "git merge" command in an interactive
98 # session will start an editor when it automatically resolves the
99 # merge for the user to explain the resulting commit, just like the
100 # "git commit" command does when it wasn't given a commit message.
102 # If you have a script that runs "git merge" and keeps its standard
103 # input and output attached to the user's terminal, and if you do not
104 # want the user to explain the resulting merge commits, you can
105 # export GIT_MERGE_AUTOEDIT environment variable set to "no", like
109 # GIT_MERGE_AUTOEDIT=no
110 # export GIT_MERGE_AUTOEDIT
112 # to disable this behavior (if you want your users to explain their
113 # merge commits, you do not have to do anything). Alternatively, you
114 # can give the "--no-edit" option to individual invocations of the
115 # "git merge" command if you know everybody who uses your script has
116 # Git v1.7.8 or newer.
118 # Longer-term: Use `git merge --no-commit` so that we always
119 # have a chance to explain our merges.
120 compat
.setenv('GIT_MERGE_AUTOEDIT', 'no')
123 def get_icon_themes():
124 """Return the default icon theme names"""
127 icon_themes_env
= core
.getenv('GIT_COLA_ICON_THEME')
129 themes
.extend([x
for x
in icon_themes_env
.split(':') if x
])
131 icon_themes_cfg
= gitcfg
.current().get_all('cola.icontheme')
133 themes
.extend(icon_themes_cfg
)
136 themes
.append('light')
141 # style note: we use camelCase here since we're masquerading a Qt class
142 class ColaApplication(object):
143 """The main cola application
145 ColaApplication handles i18n of user-visible data
148 def __init__(self
, argv
, locale
=None, gui
=True, icon_themes
=None):
153 icons
.install(icon_themes
or get_icon_themes())
155 fsmonitor
.current().files_changed
.connect(self
._update
_files
)
158 self
._app
= current(tuple(argv
))
159 self
._app
.setWindowIcon(icons
.cola())
160 self
._install
_style
()
162 self
._app
= QtCore
.QCoreApplication(argv
)
164 def _install_style(self
):
165 palette
= self
._app
.palette()
166 window
= palette
.color(QtGui
.QPalette
.Window
)
167 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
168 shadow
= palette
.color(QtGui
.QPalette
.Shadow
)
169 base
= palette
.color(QtGui
.QPalette
.Base
)
171 window_rgb
= qtutils
.rgb_css(window
)
172 highlight_rgb
= qtutils
.rgb_css(highlight
)
173 shadow_rgb
= qtutils
.rgb_css(shadow
)
174 base_rgb
= qtutils
.rgb_css(base
)
176 self
._app
.setStyleSheet("""
177 QCheckBox::indicator {
178 width: %(checkbox_size)spx;
179 height: %(checkbox_size)spx;
181 QCheckBox::indicator::unchecked {
182 border: %(checkbox_border)spx solid %(shadow_rgb)s;
183 background: %(base_rgb)s;
185 QCheckBox::indicator::checked {
186 image: url(%(checkbox_icon)s);
187 border: %(checkbox_border)spx solid %(shadow_rgb)s;
188 background: %(base_rgb)s;
191 QRadioButton::indicator {
192 width: %(radio_size)spx;
193 height: %(radio_size)spx;
195 QRadioButton::indicator::unchecked {
196 border: %(radio_border)spx solid %(shadow_rgb)s;
197 border-radius: %(radio_radius)spx;
198 background: %(base_rgb)s;
200 QRadioButton::indicator::checked {
201 image: url(%(radio_icon)s);
202 border: %(radio_border)spx solid %(shadow_rgb)s;
203 border-radius: %(radio_radius)spx;
204 background: %(base_rgb)s;
207 QSplitter::handle:hover {
208 background: %(highlight_rgb)s;
211 QMainWindow::separator {
212 background: %(window_rgb)s;
213 width: %(separator)spx;
214 height: %(separator)spx;
216 QMainWindow::separator:hover {
217 background: %(highlight_rgb)s;
220 """ % dict(separator
=defs
.separator
,
221 window_rgb
=window_rgb
,
222 highlight_rgb
=highlight_rgb
,
223 shadow_rgb
=shadow_rgb
,
225 checkbox_border
=defs
.border
,
226 checkbox_icon
=icons
.check_name(),
227 checkbox_size
=defs
.checkbox
,
228 radio_border
=defs
.radio_border
,
229 radio_icon
=icons
.dot_name(),
230 radio_radius
=defs
.checkbox
//2,
231 radio_size
=defs
.checkbox
))
233 def activeWindow(self
):
234 """Wrap activeWindow()"""
235 return self
._app
.activeWindow()
238 return self
._app
.desktop()
242 return self
._app
.exec_()
244 def set_view(self
, view
):
245 if hasattr(self
._app
, 'view'):
246 self
._app
.view
= view
248 def _update_files(self
):
249 # Respond to file system updates
250 cmds
.do(cmds
.Refresh
)
255 return ColaQApplication(list(argv
))
258 class ColaQApplication(QtWidgets
.QApplication
):
260 def __init__(self
, argv
):
261 super(ColaQApplication
, self
).__init
__(argv
)
262 self
.view
= None # injected by application_start()
265 if e
.type() == QtCore
.QEvent
.ApplicationActivate
:
266 cfg
= gitcfg
.current()
267 if cfg
.get('cola.refreshonfocus', False):
268 cmds
.do(cmds
.Refresh
)
269 return super(ColaQApplication
, self
).event(e
)
271 def commitData(self
, session_mgr
):
272 """Save session data"""
273 if self
.view
is None:
275 sid
= session_mgr
.sessionId()
276 skey
= session_mgr
.sessionKey()
277 session_id
= '%s_%s' % (sid
, skey
)
278 session
= Session(session_id
, repo
=core
.getcwd())
279 self
.view
.save_state(settings
=session
)
282 def process_args(args
):
284 # Accept 'git cola --version' or 'git cola version'
285 version
.print_version()
286 sys
.exit(core
.EXIT_SUCCESS
)
288 # Handle session management
289 restore_session(args
)
291 # Bail out if --repo is not a directory
292 repo
= core
.decode(args
.repo
)
293 if repo
.startswith('file:'):
294 repo
= repo
[len('file:'):]
295 repo
= core
.realpath(repo
)
296 if not core
.isdir(repo
):
297 errmsg
= N_('fatal: "%s" is not a directory. '
298 'Please specify a correct --repo <path>.') % repo
300 sys
.exit(core
.EXIT_USAGE
)
303 def restore_session(args
):
304 # args.settings is provided when restoring from a session.
306 if args
.session
is None:
308 session
= Session(args
.session
)
310 args
.settings
= session
311 args
.repo
= session
.repo
314 def application_init(args
, update
=False):
315 """Parses the command-line arguments and starts git-cola
317 # Ensure that we're working in a valid git repository.
318 # If not, try to find one. When found, chdir there.
322 app
= new_application(args
)
323 model
= new_model(app
, args
.repo
,
324 prompt
=args
.prompt
, settings
=args
.settings
)
326 model
.update_status()
327 cfg
= gitcfg
.current()
328 return ApplicationContext(args
, app
, cfg
, model
)
331 def application_start(context
, view
):
332 """Show the GUI and start the main event loop"""
333 # Store the view for session management
334 context
.app
.set_view(view
)
336 # Make sure that we start out on top
340 # Scan for the first time
341 runtask
= qtutils
.RunTask(parent
=view
)
342 init_update_task(view
, runtask
, context
.model
)
344 # Start the filesystem monitor thread
345 fsmonitor
.current().start()
347 QtCore
.QTimer
.singleShot(0, _send_msg
)
349 # Start the event loop
350 result
= context
.app
.exec_()
353 fsmonitor
.current().stop()
354 QtCore
.QThreadPool
.globalInstance().waitForDone()
359 def add_common_arguments(parser
):
360 # We also accept 'git cola version'
361 parser
.add_argument('--version', default
=False, action
='store_true',
362 help='print version number')
364 # Specifies a git repository to open
365 parser
.add_argument('-r', '--repo', metavar
='<repo>', default
=core
.getcwd(),
366 help='open the specified git repository')
368 # Specifies that we should prompt for a repository at startup
369 parser
.add_argument('--prompt', action
='store_true', default
=False,
370 help='prompt for a repository')
372 # Specify the icon theme
373 parser
.add_argument('--icon-theme', metavar
='<theme>',
374 dest
='icon_themes', action
='append', default
=[],
375 help='specify an icon theme (name or directory)')
377 # Resume an X Session Management session
378 parser
.add_argument('-session', metavar
='<session>', default
=None,
379 help=argparse
.SUPPRESS
)
382 def new_application(args
):
384 return ColaApplication(sys
.argv
, icon_themes
=args
.icon_themes
)
387 def new_model(app
, repo
, prompt
=False, settings
=None):
391 valid
= model
.set_worktree(repo
)
393 # We are not currently in a git repository so we need to find one.
394 # Before prompting the user for a repository, check if they've
395 # configured a default repository and attempt to use it.
396 default_repo
= gitcfg
.current().get('cola.defaultrepo')
398 valid
= model
.set_worktree(default_repo
)
401 # If we've gotten into this loop then that means that neither the
402 # current directory nor the default repository were available.
403 # Prompt the user for a repository.
404 startup_dlg
= startup
.StartupDialog(app
.activeWindow(),
406 gitdir
= startup_dlg
.find_git_repo()
408 sys
.exit(core
.EXIT_NOINPUT
)
409 valid
= model
.set_worktree(gitdir
)
414 def init_update_task(parent
, runtask
, model
):
415 """Update the model in the background
417 git-cola should startup as quickly as possible.
422 model
.update_status(update_index
=True)
424 task
= qtutils
.SimpleTask(parent
, update_status
)
429 trace
= git
.GIT_COLA_TRACE
430 if trace
== '2' or trace
== 'trace':
431 msg1
= 'info: debug level 2: trace mode enabled'
432 msg2
= 'info: set GIT_COLA_TRACE=1 for less-verbose output'
433 Interaction
.log(msg1
)
434 Interaction
.log(msg2
)
436 msg1
= 'info: debug level 1'
437 msg2
= 'info: set GIT_COLA_TRACE=2 for trace mode'
438 Interaction
.log(msg1
)
439 Interaction
.log(msg2
)
442 class ApplicationContext(object):
444 def __init__(self
, args
, app
, cfg
, model
):