status: do not exit diff mode when unselecting a file
[git-cola.git] / cola / guicmds.py
blob2debf3c903b52bd3b30766fa5cc5b84d8501799d
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 import os
4 from qtpy import QtGui
6 from . import cmds
7 from . import core
8 from . import difftool
9 from . import display
10 from . import gitcmds
11 from . import icons
12 from . import qtutils
13 from .i18n import N_
14 from .interaction import Interaction
15 from .widgets import completion
16 from .widgets import editremotes
17 from .widgets import switcher
18 from .widgets.browse import BrowseBranch
19 from .widgets.selectcommits import select_commits
20 from .widgets.selectcommits import select_commits_and_output
23 def delete_branch(context):
24 """Launch the 'Delete Branch' dialog."""
25 icon = icons.discard()
26 branch = choose_branch(context, N_('Delete Branch'), N_('Delete'), icon=icon)
27 if not branch:
28 return
29 cmds.do(cmds.DeleteBranch, context, branch)
32 def delete_remote_branch(context):
33 """Launch the 'Delete Remote Branch' dialog."""
34 remote_branch = choose_remote_branch(
35 context, N_('Delete Remote Branch'), N_('Delete'), icon=icons.discard()
37 if not remote_branch:
38 return
39 remote, branch = gitcmds.parse_remote_branch(remote_branch)
40 if remote and branch:
41 cmds.do(cmds.DeleteRemoteBranch, context, remote, branch)
44 def browse_current(context):
45 """Launch the 'Browse Current Branch' dialog."""
46 branch = gitcmds.current_branch(context)
47 BrowseBranch.browse(context, branch)
50 def browse_other(context):
51 """Prompt for a branch and inspect content at that point in time."""
52 # Prompt for a branch to browse
53 branch = choose_ref(context, N_('Browse Commits...'), N_('Browse'))
54 if not branch:
55 return
56 BrowseBranch.browse(context, branch)
59 def checkout_branch(context):
60 """Launch the 'Checkout Branch' dialog."""
61 branch = choose_potential_branch(context, N_('Checkout Branch'), N_('Checkout'))
62 if not branch:
63 return
64 cmds.do(cmds.CheckoutBranch, context, branch)
67 def cherry_pick(context):
68 """Launch the 'Cherry-Pick' dialog."""
69 revs, summaries = gitcmds.log_helper(context, all=True)
70 commits = select_commits(
71 context, N_('Cherry-Pick Commit'), revs, summaries, multiselect=False
73 if not commits:
74 return
75 cmds.do(cmds.CherryPick, context, commits)
78 def new_repo(context):
79 """Prompt for a new directory and create a new Git repository
81 :returns str: repository path or None if no repository was created.
83 """
84 git = context.git
85 path = qtutils.opendir_dialog(N_('New Repository...'), core.getcwd())
86 if not path:
87 return None
88 # Avoid needlessly calling `git init`.
89 if git.is_git_repository(path):
90 # We could prompt here and confirm that they really didn't
91 # mean to open an existing repository, but I think
92 # treating it like an "Open" is a sensible DWIM answer.
93 return path
95 status, out, err = git.init(path)
96 if status == 0:
97 return path
99 title = N_('Error Creating Repository')
100 Interaction.command_error(title, 'git init', status, out, err)
101 return None
104 def open_new_repo(context):
105 """Create a new repository and open it"""
106 dirname = new_repo(context)
107 if not dirname:
108 return
109 cmds.do(cmds.OpenRepo, context, dirname)
112 def new_bare_repo(context):
113 """Create a bare repository and configure a remote pointing to it"""
114 result = None
115 repo = prompt_for_new_bare_repo()
116 if not repo:
117 return result
118 # Create bare repo
119 ok = cmds.do(cmds.NewBareRepo, context, repo)
120 if not ok:
121 return result
122 # Add a new remote pointing to the bare repo
123 parent = qtutils.active_window()
124 add_remote = editremotes.add_remote(
125 context, parent, name=os.path.basename(repo), url=repo, readonly_url=True
127 if add_remote:
128 result = repo
130 return result
133 def prompt_for_new_bare_repo():
134 """Prompt for a directory and name for a new bare repository"""
135 path = qtutils.opendir_dialog(N_('Select Directory...'), core.getcwd())
136 if not path:
137 return None
139 bare_repo = None
140 default = os.path.basename(core.getcwd())
141 if not default.endswith('.git'):
142 default += '.git'
143 while not bare_repo:
144 name, ok = qtutils.prompt(
145 N_('Enter a name for the new bare repo'),
146 title=N_('New Bare Repository...'),
147 text=default,
149 if not name or not ok:
150 return None
151 if not name.endswith('.git'):
152 name += '.git'
153 repo = os.path.join(path, name)
154 if core.isdir(repo):
155 Interaction.critical(N_('Error'), N_('"%s" already exists') % repo)
156 else:
157 bare_repo = repo
159 return bare_repo
162 def export_patches(context):
163 """Run 'git format-patch' on a list of commits."""
164 revs, summaries = gitcmds.log_helper(context)
165 to_export_and_output = select_commits_and_output(
166 context, N_('Export Patches'), revs, summaries
168 if not to_export_and_output['to_export']:
169 return
171 cmds.do(
172 cmds.FormatPatch,
173 context,
174 reversed(to_export_and_output['to_export']),
175 reversed(revs),
176 to_export_and_output['output'],
180 def diff_against_commit(context):
181 """Diff against any commit and checkout changes using the Diff Editor"""
182 icon = icons.compare()
183 ref = choose_ref(context, N_('Diff Against Commit'), N_('Diff'), icon=icon)
184 if not ref:
185 return
186 cmds.do(cmds.DiffAgainstCommitMode, context, ref)
189 def diff_expression(context):
190 """Diff using an arbitrary expression."""
191 tracked = gitcmds.tracked_branch(context)
192 current = gitcmds.current_branch(context)
193 if tracked and current:
194 ref = tracked + '..' + current
195 else:
196 ref = '@{upstream}..'
197 difftool.diff_expression(context, qtutils.active_window(), ref)
200 def open_repo(context):
201 """Open a repository in the current window"""
202 model = context.model
203 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
204 if not dirname:
205 return
206 cmds.do(cmds.OpenRepo, context, dirname)
209 def open_repo_in_new_window(context):
210 """Spawn a new cola session."""
211 model = context.model
212 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
213 if not dirname:
214 return
215 cmds.do(cmds.OpenNewRepo, context, dirname)
218 def open_quick_repo_search(context, parent=None):
219 """Open a Quick Repository Search dialog"""
220 if parent is None:
221 parent = qtutils.active_window()
222 settings = context.settings
223 items = settings.bookmarks + settings.recent
225 if items:
226 cfg = context.cfg
227 default_repo = cfg.get('cola.defaultrepo')
229 entries = QtGui.QStandardItemModel()
230 added = set()
231 normalize = display.normalize_path
232 star_icon = icons.star()
233 folder_icon = icons.folder()
235 for item in items:
236 key = normalize(item['path'])
237 if key in added:
238 continue
240 name = item['name']
241 if default_repo == item['path']:
242 icon = star_icon
243 else:
244 icon = folder_icon
246 entry = switcher.switcher_item(key, icon, name)
247 entries.appendRow(entry)
248 added.add(key)
250 title = N_('Quick Open Repository')
251 place_holder = N_('Search repositories by name...')
252 switcher.switcher_inner_view(
253 context,
254 entries,
255 title,
256 place_holder=place_holder,
257 enter_action=lambda entry: cmds.do(cmds.OpenRepo, context, entry.key),
258 parent=parent,
262 def load_commitmsg(context):
263 """Load a commit message from a file."""
264 model = context.model
265 filename = qtutils.open_file(N_('Load Commit Message'), directory=model.getcwd())
266 if filename:
267 cmds.do(cmds.LoadCommitMessageFromFile, context, filename)
270 def choose_from_dialog(get, context, title, button_text, default, icon=None):
271 """Choose a value from a dialog using the `get` method"""
272 parent = qtutils.active_window()
273 return get(context, title, button_text, parent, default=default, icon=icon)
276 def choose_ref(context, title, button_text, default=None, icon=None):
277 """Choose a Git ref and return it"""
278 return choose_from_dialog(
279 completion.GitRefDialog.get, context, title, button_text, default, icon=icon
283 def choose_branch(context, title, button_text, default=None, icon=None):
284 """Choose a branch and return either the chosen branch or an empty value"""
285 return choose_from_dialog(
286 completion.GitBranchDialog.get, context, title, button_text, default, icon=icon
290 def choose_potential_branch(context, title, button_text, default=None, icon=None):
291 """Choose a "potential" branch for checking out.
293 This dialog includes remote branches from which new local branches can be created.
295 return choose_from_dialog(
296 completion.GitCheckoutBranchDialog.get,
297 context,
298 title,
299 button_text,
300 default,
301 icon=icon,
305 def choose_remote_branch(context, title, button_text, default=None, icon=None):
306 """Choose a remote branch"""
307 return choose_from_dialog(
308 completion.GitRemoteBranchDialog.get,
309 context,
310 title,
311 button_text,
312 default,
313 icon=icon,
317 def review_branch(context):
318 """Diff against an arbitrary revision, branch, tag, etc."""
319 branch = choose_ref(context, N_('Select Branch to Review'), N_('Review'))
320 if not branch:
321 return
322 merge_base = gitcmds.merge_base_parent(context, branch)
323 difftool.diff_commits(context, qtutils.active_window(), merge_base, branch)
326 def rename_branch(context):
327 """Launch the 'Rename Branch' dialogs."""
328 branch = choose_branch(context, N_('Rename Existing Branch'), N_('Select'))
329 if not branch:
330 return
331 new_branch = choose_branch(context, N_('Enter New Branch Name'), N_('Rename'))
332 if not new_branch:
333 return
334 cmds.do(cmds.RenameBranch, context, branch, new_branch)
337 def reset_soft(context):
338 """Run "git reset --soft" to reset the branch HEAD"""
339 title = N_('Reset Branch (Soft)')
340 ok_text = N_('Reset Branch')
341 ref = choose_ref(context, title, ok_text, default='HEAD^')
342 if ref:
343 cmds.do(cmds.ResetSoft, context, ref)
346 def reset_mixed(context):
347 """Run "git reset --mixed" to reset the branch HEAD and staging area"""
348 title = N_('Reset Branch and Stage (Mixed)')
349 ok_text = N_('Reset')
350 ref = choose_ref(context, title, ok_text, default='HEAD^')
351 if ref:
352 cmds.do(cmds.ResetMixed, context, ref)
355 def reset_keep(context):
356 """Run "git reset --keep" safe reset to avoid clobbering local changes"""
357 title = N_('Reset All (Keep Unstaged Changes)')
358 ref = choose_ref(context, title, N_('Reset and Restore'))
359 if ref:
360 cmds.do(cmds.ResetKeep, context, ref)
363 def reset_merge(context):
364 """Run "git reset --merge" to reset the working tree and staging area
366 The staging area is allowed to carry forward unmerged index entries,
367 but if any unstaged changes would be clobbered by the reset then the
368 reset is aborted.
370 title = N_('Restore Worktree and Reset All (Merge)')
371 ok_text = N_('Reset and Restore')
372 ref = choose_ref(context, title, ok_text, default='HEAD^')
373 if ref:
374 cmds.do(cmds.ResetMerge, context, ref)
377 def reset_hard(context):
378 """Run "git reset --hard" to fully reset the working tree and staging area"""
379 title = N_('Restore Worktree and Reset All (Hard)')
380 ok_text = N_('Reset and Restore')
381 ref = choose_ref(context, title, ok_text, default='HEAD^')
382 if ref:
383 cmds.do(cmds.ResetHard, context, ref)
386 def restore_worktree(context):
387 """Restore the worktree to the content from the specified commit"""
388 title = N_('Restore Worktree')
389 ok_text = N_('Restore Worktree')
390 ref = choose_ref(context, title, ok_text, default='HEAD^')
391 if ref:
392 cmds.do(cmds.RestoreWorktree, context, ref)
395 def install():
396 """Install the GUI-model interaction hooks"""
397 Interaction.choose_ref = staticmethod(choose_ref)