dag: use a consistent sort order when selecting commits
[git-cola.git] / cola / guicmds.py
blobc756742adba4d0a8bb28dce94b2d6b984f639889
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, default=None):
60 """Launch the 'Checkout Branch' dialog."""
61 branch = choose_potential_branch(
62 context, N_('Checkout Branch'), N_('Checkout'), default=default
64 if not branch:
65 return
66 cmds.do(cmds.CheckoutBranch, context, branch)
69 def cherry_pick(context):
70 """Launch the 'Cherry-Pick' dialog."""
71 revs, summaries = gitcmds.log_helper(context, all=True)
72 commits = select_commits(
73 context, N_('Cherry-Pick Commit'), revs, summaries, multiselect=False
75 if not commits:
76 return
77 cmds.do(cmds.CherryPick, context, commits)
80 def new_repo(context):
81 """Prompt for a new directory and create a new Git repository
83 :returns str: repository path or None if no repository was created.
85 """
86 git = context.git
87 path = qtutils.opendir_dialog(N_('New Repository...'), core.getcwd())
88 if not path:
89 return None
90 # Avoid needlessly calling `git init`.
91 if git.is_git_repository(path):
92 # We could prompt here and confirm that they really didn't
93 # mean to open an existing repository, but I think
94 # treating it like an "Open" is a sensible DWIM answer.
95 return path
97 status, out, err = git.init(path)
98 if status == 0:
99 return path
101 title = N_('Error Creating Repository')
102 Interaction.command_error(title, 'git init', status, out, err)
103 return None
106 def open_new_repo(context):
107 """Create a new repository and open it"""
108 dirname = new_repo(context)
109 if not dirname:
110 return
111 cmds.do(cmds.OpenRepo, context, dirname)
114 def new_bare_repo(context):
115 """Create a bare repository and configure a remote pointing to it"""
116 result = None
117 repo = prompt_for_new_bare_repo()
118 if not repo:
119 return result
120 # Create bare repo
121 ok = cmds.do(cmds.NewBareRepo, context, repo)
122 if not ok:
123 return result
124 # Add a new remote pointing to the bare repo
125 parent = qtutils.active_window()
126 add_remote = editremotes.add_remote(
127 context, parent, name=os.path.basename(repo), url=repo, readonly_url=True
129 if add_remote:
130 result = repo
132 return result
135 def prompt_for_new_bare_repo():
136 """Prompt for a directory and name for a new bare repository"""
137 path = qtutils.opendir_dialog(N_('Select Directory...'), core.getcwd())
138 if not path:
139 return None
141 bare_repo = None
142 default = os.path.basename(core.getcwd())
143 if not default.endswith('.git'):
144 default += '.git'
145 while not bare_repo:
146 name, ok = qtutils.prompt(
147 N_('Enter a name for the new bare repo'),
148 title=N_('New Bare Repository...'),
149 text=default,
151 if not name or not ok:
152 return None
153 if not name.endswith('.git'):
154 name += '.git'
155 repo = os.path.join(path, name)
156 if core.isdir(repo):
157 Interaction.critical(N_('Error'), N_('"%s" already exists') % repo)
158 else:
159 bare_repo = repo
161 return bare_repo
164 def export_patches(context):
165 """Run 'git format-patch' on a list of commits."""
166 revs, summaries = gitcmds.log_helper(context)
167 to_export_and_output = select_commits_and_output(
168 context, N_('Export Patches'), revs, summaries
170 if not to_export_and_output['to_export']:
171 return
173 cmds.do(
174 cmds.FormatPatch,
175 context,
176 reversed(to_export_and_output['to_export']),
177 reversed(revs),
178 to_export_and_output['output'],
182 def diff_against_commit(context):
183 """Diff against any commit and checkout changes using the Diff Editor"""
184 icon = icons.compare()
185 ref = choose_ref(context, N_('Diff Against Commit'), N_('Diff'), icon=icon)
186 if not ref:
187 return
188 cmds.do(cmds.DiffAgainstCommitMode, context, ref)
191 def diff_expression(context):
192 """Diff using an arbitrary expression."""
193 tracked = gitcmds.tracked_branch(context)
194 current = gitcmds.current_branch(context)
195 if tracked and current:
196 ref = tracked + '..' + current
197 else:
198 ref = '@{upstream}..'
199 difftool.diff_expression(context, qtutils.active_window(), ref)
202 def open_repo(context):
203 """Open a repository in the current window"""
204 model = context.model
205 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
206 if not dirname:
207 return
208 cmds.do(cmds.OpenRepo, context, dirname)
211 def open_repo_in_new_window(context):
212 """Spawn a new cola session."""
213 model = context.model
214 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
215 if not dirname:
216 return
217 cmds.do(cmds.OpenNewRepo, context, dirname)
220 def open_quick_repo_search(context, parent=None):
221 """Open a Quick Repository Search dialog"""
222 if parent is None:
223 parent = qtutils.active_window()
224 settings = context.settings
225 items = settings.bookmarks + settings.recent
227 if items:
228 cfg = context.cfg
229 default_repo = cfg.get('cola.defaultrepo')
231 entries = QtGui.QStandardItemModel()
232 added = set()
233 normalize = display.normalize_path
234 star_icon = icons.star()
235 folder_icon = icons.folder()
237 for item in items:
238 key = normalize(item['path'])
239 if key in added:
240 continue
242 name = item['name']
243 if default_repo == item['path']:
244 icon = star_icon
245 else:
246 icon = folder_icon
248 entry = switcher.switcher_item(key, icon, name)
249 entries.appendRow(entry)
250 added.add(key)
252 title = N_('Quick Open Repository')
253 place_holder = N_('Search repositories by name...')
254 switcher.switcher_inner_view(
255 context,
256 entries,
257 title,
258 place_holder=place_holder,
259 enter_action=lambda entry: cmds.do(cmds.OpenRepo, context, entry.key),
260 parent=parent,
264 def load_commitmsg(context):
265 """Load a commit message from a file."""
266 model = context.model
267 filename = qtutils.open_file(N_('Load Commit Message'), directory=model.getcwd())
268 if filename:
269 cmds.do(cmds.LoadCommitMessageFromFile, context, filename)
272 def choose_from_dialog(get, context, title, button_text, default, icon=None):
273 """Choose a value from a dialog using the `get` method"""
274 parent = qtutils.active_window()
275 return get(context, title, button_text, parent, default=default, icon=icon)
278 def choose_ref(context, title, button_text, default=None, icon=None):
279 """Choose a Git ref and return it"""
280 return choose_from_dialog(
281 completion.GitRefDialog.get, context, title, button_text, default, icon=icon
285 def choose_branch(context, title, button_text, default=None, icon=None):
286 """Choose a branch and return either the chosen branch or an empty value"""
287 return choose_from_dialog(
288 completion.GitBranchDialog.get, context, title, button_text, default, icon=icon
292 def choose_potential_branch(context, title, button_text, default=None, icon=None):
293 """Choose a "potential" branch for checking out.
295 This dialog includes remote branches from which new local branches can be created.
297 return choose_from_dialog(
298 completion.GitCheckoutBranchDialog.get,
299 context,
300 title,
301 button_text,
302 default,
303 icon=icon,
307 def choose_remote_branch(context, title, button_text, default=None, icon=None):
308 """Choose a remote branch"""
309 return choose_from_dialog(
310 completion.GitRemoteBranchDialog.get,
311 context,
312 title,
313 button_text,
314 default,
315 icon=icon,
319 def review_branch(context):
320 """Diff against an arbitrary revision, branch, tag, etc."""
321 branch = choose_ref(context, N_('Select Branch to Review'), N_('Review'))
322 if not branch:
323 return
324 merge_base = gitcmds.merge_base_parent(context, branch)
325 difftool.diff_commits(context, qtutils.active_window(), merge_base, branch)
328 def rename_branch(context):
329 """Launch the 'Rename Branch' dialogs."""
330 branch = choose_branch(context, N_('Rename Existing Branch'), N_('Select'))
331 if not branch:
332 return
333 new_branch = choose_branch(context, N_('Enter New Branch Name'), N_('Rename'))
334 if not new_branch:
335 return
336 cmds.do(cmds.RenameBranch, context, branch, new_branch)
339 def reset_soft(context):
340 """Run "git reset --soft" to reset the branch HEAD"""
341 title = N_('Reset Branch (Soft)')
342 ok_text = N_('Reset Branch')
343 ref = choose_ref(context, title, ok_text, default='HEAD^')
344 if ref:
345 cmds.do(cmds.ResetSoft, context, ref)
348 def reset_mixed(context):
349 """Run "git reset --mixed" to reset the branch HEAD and staging area"""
350 title = N_('Reset Branch and Stage (Mixed)')
351 ok_text = N_('Reset')
352 ref = choose_ref(context, title, ok_text, default='HEAD^')
353 if ref:
354 cmds.do(cmds.ResetMixed, context, ref)
357 def reset_keep(context):
358 """Run "git reset --keep" safe reset to avoid clobbering local changes"""
359 title = N_('Reset All (Keep Unstaged Changes)')
360 ref = choose_ref(context, title, N_('Reset and Restore'))
361 if ref:
362 cmds.do(cmds.ResetKeep, context, ref)
365 def reset_merge(context):
366 """Run "git reset --merge" to reset the working tree and staging area
368 The staging area is allowed to carry forward unmerged index entries,
369 but if any unstaged changes would be clobbered by the reset then the
370 reset is aborted.
372 title = N_('Restore Worktree and Reset All (Merge)')
373 ok_text = N_('Reset and Restore')
374 ref = choose_ref(context, title, ok_text, default='HEAD^')
375 if ref:
376 cmds.do(cmds.ResetMerge, context, ref)
379 def reset_hard(context):
380 """Run "git reset --hard" to fully reset the working tree and staging area"""
381 title = N_('Restore Worktree and Reset All (Hard)')
382 ok_text = N_('Reset and Restore')
383 ref = choose_ref(context, title, ok_text, default='HEAD^')
384 if ref:
385 cmds.do(cmds.ResetHard, context, ref)
388 def restore_worktree(context):
389 """Restore the worktree to the content from the specified commit"""
390 title = N_('Restore Worktree')
391 ok_text = N_('Restore Worktree')
392 ref = choose_ref(context, title, ok_text, default='HEAD^')
393 if ref:
394 cmds.do(cmds.RestoreWorktree, context, ref)
397 def install():
398 """Install the GUI-model interaction hooks"""
399 Interaction.choose_ref = staticmethod(choose_ref)