ui: use the icons and qtutils helpers
[git-cola.git] / cola / guicmds.py
blobdf49abe4abb470a268be37be3d164f54502bfda3
1 from __future__ import division, absolute_import, unicode_literals
3 import os
4 import re
6 from PyQt4 import QtCore
7 from PyQt4 import QtGui
8 from PyQt4.QtCore import SIGNAL
10 from cola import cmds
11 from cola import core
12 from cola import difftool
13 from cola import gitcmds
14 from cola import icons
15 from cola import qtutils
16 from cola import utils
17 from cola.git import git
18 from cola.i18n import N_
19 from cola.interaction import Interaction
20 from cola.models import main
21 from cola.widgets import completion
22 from cola.widgets.browse import BrowseDialog
23 from cola.widgets.selectcommits import select_commits
24 from cola.compat import ustr
27 def delete_branch():
28 """Launch the 'Delete Branch' dialog."""
29 icon = icons.discard()
30 branch = choose_branch(N_('Delete Branch'), N_('Delete'), icon=icon)
31 if not branch:
32 return
33 cmds.do(cmds.DeleteBranch, branch)
36 def delete_remote_branch():
37 """Launch the 'Delete Remote Branch' dialog."""
38 branch = choose_remote_branch(N_('Delete Remote Branch'), N_('Delete'),
39 icon=icons.discard())
40 if not branch:
41 return
42 rgx = re.compile(r'^(?P<remote>[^/]+)/(?P<branch>.+)$')
43 match = rgx.match(branch)
44 if match:
45 remote = match.group('remote')
46 branch = match.group('branch')
47 cmds.do(cmds.DeleteRemoteBranch, remote, branch)
50 def browse_current():
51 """Launch the 'Browse Current Branch' dialog."""
52 branch = gitcmds.current_branch()
53 BrowseDialog.browse(branch)
56 def browse_other():
57 """Prompt for a branch and inspect content at that point in time."""
58 # Prompt for a branch to browse
59 branch = choose_ref(N_('Browse Commits...'), N_('Browse'))
60 if not branch:
61 return
62 BrowseDialog.browse(branch)
65 def checkout_branch():
66 """Launch the 'Checkout Branch' dialog."""
67 branch = choose_branch(N_('Checkout Branch'), N_('Checkout'))
68 if not branch:
69 return
70 cmds.do(cmds.CheckoutBranch, branch)
73 def cherry_pick():
74 """Launch the 'Cherry-Pick' dialog."""
75 revs, summaries = gitcmds.log_helper(all=True)
76 commits = select_commits(N_('Cherry-Pick Commit'),
77 revs, summaries, multiselect=False)
78 if not commits:
79 return
80 cmds.do(cmds.CherryPick, commits)
83 def new_repo():
84 """Prompt for a new directory and create a new Git repository
86 :returns str: repository path or None if no repository was created.
88 """
89 dlg = QtGui.QFileDialog()
90 dlg.setFileMode(QtGui.QFileDialog.Directory)
91 dlg.setOption(QtGui.QFileDialog.ShowDirsOnly)
92 dlg.show()
93 dlg.raise_()
94 if dlg.exec_() != QtGui.QFileDialog.Accepted:
95 return None
96 paths = dlg.selectedFiles()
97 if not paths:
98 return None
99 path = ustr(paths[0])
100 if not path:
101 return None
102 # Avoid needlessly calling `git init`.
103 if git.is_git_dir(path):
104 # We could prompt here and confirm that they really didn't
105 # mean to open an existing repository, but I think
106 # treating it like an "Open" is a sensible DWIM answer.
107 return path
109 status, out, err = core.run_command(['git', 'init', path])
110 if status == 0:
111 return path
112 else:
113 title = N_('Error Creating Repository')
114 msg = (N_('"%(command)s" returned exit status %(status)d') %
115 dict(command='git init %s' % path, status=status))
116 details = N_('Output:\n%s') % out
117 if err:
118 details += '\n\n'
119 details += N_('Errors: %s') % err
120 qtutils.critical(title, msg, details)
121 return None
124 def open_new_repo():
125 dirname = new_repo()
126 if not dirname:
127 return
128 cmds.do(cmds.OpenRepo, dirname)
131 def prompt_for_clone():
133 Present a GUI for cloning a repository.
135 Returns the target directory and URL
138 url, ok = qtutils.prompt(N_('Path or URL to clone (Env. $VARS okay)'))
139 url = utils.expandpath(url)
140 if not ok or not url:
141 return None
142 try:
143 # Pick a suitable basename by parsing the URL
144 newurl = url.replace('\\', '/').rstrip('/')
145 default = newurl.rsplit('/', 1)[-1]
146 if default == '.git':
147 # The end of the URL is /.git, so assume it's a file path
148 default = os.path.basename(os.path.dirname(newurl))
149 if default.endswith('.git'):
150 # The URL points to a bare repo
151 default = default[:-4]
152 if url == '.':
153 # The URL is the current repo
154 default = os.path.basename(core.getcwd())
155 if not default:
156 raise
157 except:
158 Interaction.information(
159 N_('Error Cloning'),
160 N_('Could not parse Git URL: "%s"') % url)
161 Interaction.log(N_('Could not parse Git URL: "%s"') % url)
162 return None
164 # Prompt the user for a directory to use as the parent directory
165 msg = N_('Select a parent directory for the new clone')
166 dirname = qtutils.opendir_dialog(msg, main.model().getcwd())
167 if not dirname:
168 return None
169 count = 1
170 destdir = os.path.join(dirname, default)
171 olddestdir = destdir
172 if core.exists(destdir):
173 # An existing path can be specified
174 msg = (N_('"%s" already exists, cola will create a new directory') %
175 destdir)
176 Interaction.information(N_('Directory Exists'), msg)
178 # Make sure the new destdir doesn't exist
179 while core.exists(destdir):
180 destdir = olddestdir + str(count)
181 count += 1
183 return url, destdir
186 def export_patches():
187 """Run 'git format-patch' on a list of commits."""
188 revs, summaries = gitcmds.log_helper()
189 to_export = select_commits(N_('Export Patches'), revs, summaries)
190 if not to_export:
191 return
192 cmds.do(cmds.FormatPatch, reversed(to_export), reversed(revs))
195 def diff_expression():
196 """Diff using an arbitrary expression."""
197 tracked = gitcmds.tracked_branch()
198 current = gitcmds.current_branch()
199 if tracked and current:
200 ref = tracked + '..' + current
201 else:
202 ref = 'origin/master..'
203 difftool.diff_expression(qtutils.active_window(), ref)
206 def open_repo():
207 dirname = qtutils.opendir_dialog(N_('Open Git Repository...'),
208 main.model().getcwd())
209 if not dirname:
210 return
211 cmds.do(cmds.OpenRepo, dirname)
214 def open_repo_in_new_window():
215 """Spawn a new cola session."""
216 dirname = qtutils.opendir_dialog(N_('Open Git Repository...'),
217 main.model().getcwd())
218 if not dirname:
219 return
220 cmds.do(cmds.OpenNewRepo, dirname)
223 def load_commitmsg():
224 """Load a commit message from a file."""
225 filename = qtutils.open_file(N_('Load Commit Message'),
226 directory=main.model().getcwd())
227 if filename:
228 cmds.do(cmds.LoadCommitMessageFromFile, filename)
231 def choose_from_dialog(get, title, button_text, default, icon=None):
232 parent = qtutils.active_window()
233 return get(title, button_text, parent, default=default, icon=icon)
236 def choose_ref(title, button_text, default=None):
237 return choose_from_dialog(completion.GitRefDialog.get,
238 title, button_text, default)
241 def choose_branch(title, button_text, default=None, icon=None):
242 return choose_from_dialog(completion.GitBranchDialog.get,
243 title, button_text, default, icon=icon)
246 def choose_remote_branch(title, button_text, default=None, icon=None):
247 return choose_from_dialog(completion.GitRemoteBranchDialog.get,
248 title, button_text, default, icon=icon)
251 def review_branch():
252 """Diff against an arbitrary revision, branch, tag, etc."""
253 branch = choose_ref(N_('Select Branch to Review'), N_('Review'))
254 if not branch:
255 return
256 merge_base = gitcmds.merge_base_parent(branch)
257 difftool.diff_commits(qtutils.active_window(), merge_base, branch)
260 class TaskRunner(QtCore.QObject):
261 """Runs QRunnable instances and transfers control when they finish"""
263 def __init__(self, parent):
264 QtCore.QObject.__init__(self, parent)
265 self.tasks = []
266 self.task_details = {}
267 self.connect(self, Task.FINISHED, self.finish)
269 def start(self, task, progress=None, finish=None):
270 """Start the task and register a callback"""
271 if progress is not None:
272 progress.show()
273 # prevents garbage collection bugs in certain PyQt4 versions
274 self.tasks.append(task)
275 task_id = id(task)
276 self.task_details[task_id] = (progress, finish)
277 QtCore.QThreadPool.globalInstance().start(task)
279 def finish(self, task, *args, **kwargs):
280 task_id = id(task)
281 try:
282 self.tasks.remove(task)
283 except:
284 pass
285 try:
286 progress, finish = self.task_details[task_id]
287 del self.task_details[task_id]
288 except KeyError:
289 finish = progress = None
291 if progress is not None:
292 progress.hide()
294 if finish is not None:
295 finish(task, *args, **kwargs)
298 class Task(QtCore.QRunnable):
299 """Base class for concrete tasks"""
301 FINISHED = SIGNAL('finished')
303 def __init__(self, sender):
304 QtCore.QRunnable.__init__(self)
305 self.sender = sender
307 def finish(self, *args, **kwargs):
308 self.sender.emit(self.FINISHED, self, *args, **kwargs)
311 class CloneTask(Task):
312 """Clones a Git repository"""
314 def __init__(self, sender, url, destdir, spawn):
315 Task.__init__(self, sender)
316 self.url = url
317 self.destdir = destdir
318 self.spawn = spawn
319 self.cmd = None
321 def run(self):
322 """Runs the model action and captures the result"""
323 self.cmd = cmds.do(cmds.Clone, self.url, self.destdir,
324 spawn=self.spawn)
325 self.finish()
328 def clone_repo(task_runner, progress, finish, spawn):
329 """Clone a repostiory asynchronously with progress animation"""
330 result = prompt_for_clone()
331 if result is None:
332 return
333 # Use a thread to update in the background
334 url, destdir = result
335 progress.set_details(N_('Clone Repository'),
336 N_('Cloning repository at %s') % url)
337 task = CloneTask(task_runner, url, destdir, spawn)
338 task_runner.start(task,
339 finish=finish,
340 progress=progress)
343 def report_clone_repo_errors(task):
344 """Report errors from the clone task if they exist"""
345 if task.cmd is None or task.cmd.ok:
346 return
347 Interaction.critical(task.cmd.error_message,
348 message=task.cmd.error_message,
349 details=task.cmd.error_details)
351 def rename_branch():
352 """Launch the 'Rename Branch' dialogs."""
353 branch = choose_branch(N_('Rename Existing Branch'), N_('Select'))
354 if not branch:
355 return
356 new_branch = choose_branch(N_('Enter Branch New Name'), N_('Rename'))
357 if not new_branch:
358 return
359 cmds.do(cmds.RenameBranch, branch, new_branch)