git-cola v4.9.0
[git-cola.git] / cola / guicmds.py
blobc3174a054e389f728cfb39a91889ad2e0239ff86
1 import os
3 from qtpy import QtGui
5 from . import cmds
6 from . import core
7 from . import difftool
8 from . import display
9 from . import gitcmds
10 from . import icons
11 from . import qtutils
12 from .i18n import N_
13 from .interaction import Interaction
14 from .widgets import completion
15 from .widgets import editremotes
16 from .widgets import switcher
17 from .widgets.browse import BrowseBranch
18 from .widgets.selectcommits import select_commits
19 from .widgets.selectcommits import select_commits_and_output
22 def copy_commit_id_to_clipboard(context):
23 """Copy the current commit ID to the clipboard"""
24 status, commit_id, _ = context.git.rev_parse('HEAD')
25 if status == 0 and commit_id:
26 qtutils.set_clipboard(commit_id)
29 def delete_branch(context):
30 """Launch the 'Delete Branch' dialog."""
31 icon = icons.discard()
32 branch = choose_branch(context, N_('Delete Branch'), N_('Delete'), icon=icon)
33 if not branch:
34 return
35 cmds.do(cmds.DeleteBranch, context, branch)
38 def delete_remote_branch(context):
39 """Launch the 'Delete Remote Branch' dialog."""
40 remote_branch = choose_remote_branch(
41 context, N_('Delete Remote Branch'), N_('Delete'), icon=icons.discard()
43 if not remote_branch:
44 return
45 remote, branch = gitcmds.parse_remote_branch(remote_branch)
46 if remote and branch:
47 cmds.do(cmds.DeleteRemoteBranch, context, remote, branch)
50 def browse_current(context):
51 """Launch the 'Browse Current Branch' dialog."""
52 branch = gitcmds.current_branch(context)
53 BrowseBranch.browse(context, branch)
56 def browse_other(context):
57 """Prompt for a branch and inspect content at that point in time."""
58 # Prompt for a branch to browse
59 branch = choose_ref(context, N_('Browse Commits...'), N_('Browse'))
60 if not branch:
61 return
62 BrowseBranch.browse(context, branch)
65 def checkout_branch(context, default=None):
66 """Launch the 'Checkout Branch' dialog."""
67 branch = choose_potential_branch(
68 context, N_('Checkout Branch'), N_('Checkout'), default=default
70 if not branch:
71 return
72 cmds.do(cmds.CheckoutBranch, context, branch)
75 def cherry_pick(context):
76 """Launch the 'Cherry-Pick' dialog."""
77 revs, summaries = gitcmds.log_helper(context, all=True)
78 commits = select_commits(
79 context, N_('Cherry-Pick Commit'), revs, summaries, multiselect=False
81 if not commits:
82 return
83 cmds.do(cmds.CherryPick, context, commits)
86 def new_repo(context):
87 """Prompt for a new directory and create a new Git repository
89 :returns str: repository path or None if no repository was created.
91 """
92 git = context.git
93 path = qtutils.opendir_dialog(N_('New Repository...'), core.getcwd())
94 if not path:
95 return None
96 # Avoid needlessly calling `git init`.
97 if git.is_git_repository(path):
98 # We could prompt here and confirm that they really didn't
99 # mean to open an existing repository, but I think
100 # treating it like an "Open" is a sensible DWIM answer.
101 return path
103 status, out, err = git.init(path)
104 if status == 0:
105 return path
107 title = N_('Error Creating Repository')
108 Interaction.command_error(title, 'git init', status, out, err)
109 return None
112 def open_new_repo(context):
113 """Create a new repository and open it"""
114 dirname = new_repo(context)
115 if not dirname:
116 return
117 cmds.do(cmds.OpenRepo, context, dirname)
120 def new_bare_repo(context):
121 """Create a bare repository and configure a remote pointing to it"""
122 result = None
123 repo = prompt_for_new_bare_repo()
124 if not repo:
125 return result
126 # Create bare repo
127 ok = cmds.do(cmds.NewBareRepo, context, repo)
128 if not ok:
129 return result
130 # Add a new remote pointing to the bare repo
131 parent = qtutils.active_window()
132 add_remote = editremotes.add_remote(
133 context, parent, name=os.path.basename(repo), url=repo, readonly_url=True
135 if add_remote:
136 result = repo
138 return result
141 def prompt_for_new_bare_repo():
142 """Prompt for a directory and name for a new bare repository"""
143 path = qtutils.opendir_dialog(N_('Select Directory...'), core.getcwd())
144 if not path:
145 return None
147 bare_repo = None
148 default = os.path.basename(core.getcwd())
149 if not default.endswith('.git'):
150 default += '.git'
151 while not bare_repo:
152 name, ok = qtutils.prompt(
153 N_('Enter a name for the new bare repo'),
154 title=N_('New Bare Repository...'),
155 text=default,
157 if not name or not ok:
158 return None
159 if not name.endswith('.git'):
160 name += '.git'
161 repo = os.path.join(path, name)
162 if core.isdir(repo):
163 Interaction.critical(N_('Error'), N_('"%s" already exists') % repo)
164 else:
165 bare_repo = repo
167 return bare_repo
170 def export_patches(context):
171 """Run 'git format-patch' on a list of commits."""
172 revs, summaries = gitcmds.log_helper(context)
173 to_export_and_output = select_commits_and_output(
174 context, N_('Export Patches'), revs, summaries
176 if not to_export_and_output['to_export']:
177 return
179 cmds.do(
180 cmds.FormatPatch,
181 context,
182 reversed(to_export_and_output['to_export']),
183 reversed(revs),
184 output=to_export_and_output['output'],
188 def diff_against_commit(context):
189 """Diff against any commit and checkout changes using the Diff Editor"""
190 icon = icons.compare()
191 ref = choose_ref(context, N_('Diff Against Commit'), N_('Diff'), icon=icon)
192 if not ref:
193 return
194 cmds.do(cmds.DiffAgainstCommitMode, context, ref)
197 def diff_expression(context):
198 """Diff using an arbitrary expression."""
199 tracked = gitcmds.tracked_branch(context)
200 current = gitcmds.current_branch(context)
201 if tracked and current:
202 ref = tracked + '..' + current
203 else:
204 ref = '@{upstream}..'
205 difftool.diff_expression(context, qtutils.active_window(), ref)
208 def open_repo(context):
209 """Open a repository in the current window"""
210 model = context.model
211 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
212 if not dirname:
213 return
214 cmds.do(cmds.OpenRepo, context, dirname)
217 def open_repo_in_new_window(context):
218 """Spawn a new cola session."""
219 model = context.model
220 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
221 if not dirname:
222 return
223 cmds.do(cmds.OpenNewRepo, context, dirname)
226 def open_quick_repo_search(context, parent=None):
227 """Open a Quick Repository Search dialog"""
228 if parent is None:
229 parent = qtutils.active_window()
230 settings = context.settings
231 items = settings.bookmarks + settings.recent
233 if items:
234 cfg = context.cfg
235 default_repo = cfg.get('cola.defaultrepo')
237 entries = QtGui.QStandardItemModel()
238 added = set()
239 normalize = display.normalize_path
240 star_icon = icons.star()
241 folder_icon = icons.folder()
243 for item in items:
244 key = normalize(item['path'])
245 if key in added:
246 continue
248 name = item['name']
249 if default_repo == item['path']:
250 icon = star_icon
251 else:
252 icon = folder_icon
254 entry = switcher.switcher_item(key, icon, name)
255 entries.appendRow(entry)
256 added.add(key)
258 title = N_('Quick Open Repository')
259 place_holder = N_('Search repositories by name...')
260 switcher.switcher_inner_view(
261 context,
262 entries,
263 title,
264 place_holder=place_holder,
265 enter_action=lambda entry: cmds.do(cmds.OpenRepo, context, entry.key),
266 parent=parent,
270 def load_commitmsg(context):
271 """Load a commit message from a file."""
272 model = context.model
273 filename = qtutils.open_file(N_('Load Commit Message'), directory=model.getcwd())
274 if filename:
275 cmds.do(cmds.LoadCommitMessageFromFile, context, filename)
278 def choose_from_dialog(get, context, title, button_text, default, icon=None):
279 """Choose a value from a dialog using the `get` method"""
280 parent = qtutils.active_window()
281 return get(context, title, button_text, parent, default=default, icon=icon)
284 def choose_ref(context, title, button_text, default=None, icon=None):
285 """Choose a Git ref and return it"""
286 return choose_from_dialog(
287 completion.GitRefDialog.get, context, title, button_text, default, icon=icon
291 def choose_branch(context, title, button_text, default=None, icon=None):
292 """Choose a branch and return either the chosen branch or an empty value"""
293 return choose_from_dialog(
294 completion.GitBranchDialog.get, context, title, button_text, default, icon=icon
298 def choose_potential_branch(context, title, button_text, default=None, icon=None):
299 """Choose a "potential" branch for checking out.
301 This dialog includes remote branches from which new local branches can be created.
303 return choose_from_dialog(
304 completion.GitCheckoutBranchDialog.get,
305 context,
306 title,
307 button_text,
308 default,
309 icon=icon,
313 def choose_remote_branch(context, title, button_text, default=None, icon=None):
314 """Choose a remote branch"""
315 return choose_from_dialog(
316 completion.GitRemoteBranchDialog.get,
317 context,
318 title,
319 button_text,
320 default,
321 icon=icon,
325 def review_branch(context):
326 """Diff against an arbitrary revision, branch, tag, etc."""
327 branch = choose_ref(context, N_('Select Branch to Review'), N_('Review'))
328 if not branch:
329 return
330 merge_base = gitcmds.merge_base_parent(context, branch)
331 difftool.diff_commits(context, qtutils.active_window(), merge_base, branch)
334 def rename_branch(context):
335 """Launch the 'Rename Branch' dialogs."""
336 branch = choose_branch(context, N_('Rename Existing Branch'), N_('Select'))
337 if not branch:
338 return
339 new_branch = choose_branch(context, N_('Enter New Branch Name'), N_('Rename'))
340 if not new_branch:
341 return
342 cmds.do(cmds.RenameBranch, context, branch, new_branch)
345 def reset_soft(context):
346 """Run "git reset --soft" to reset the branch HEAD"""
347 title = N_('Reset Branch (Soft)')
348 ok_text = N_('Reset Branch')
349 default = context.settings.get_value('reset::soft', 'ref', default='HEAD^')
350 ref = choose_ref(context, title, ok_text, default=default)
351 if ref:
352 cmds.do(cmds.ResetSoft, context, ref)
353 context.settings.set_value('reset::soft', 'ref', ref)
356 def reset_mixed(context):
357 """Run "git reset --mixed" to reset the branch HEAD and staging area"""
358 title = N_('Reset Branch and Stage (Mixed)')
359 ok_text = N_('Reset')
360 default = context.settings.get_value('reset::mixed', 'ref', default='HEAD^')
361 ref = choose_ref(context, title, ok_text, default=default)
362 if ref:
363 cmds.do(cmds.ResetMixed, context, ref)
364 context.settings.set_value('reset::mixed', 'ref', ref)
367 def reset_keep(context):
368 """Run "git reset --keep" safe reset to avoid clobbering local changes"""
369 title = N_('Reset All (Keep Unstaged Changes)')
370 default = context.settings.get_value('reset::keep', 'ref', default='HEAD^')
371 ref = choose_ref(context, title, N_('Reset and Restore'), default=default)
372 if ref:
373 cmds.do(cmds.ResetKeep, context, ref)
374 context.settings.set_value('reset::keep', 'ref', ref)
377 def reset_merge(context):
378 """Run "git reset --merge" to reset the working tree and staging area
380 The staging area is allowed to carry forward unmerged index entries,
381 but if any unstaged changes would be clobbered by the reset then the
382 reset is aborted.
384 title = N_('Restore Worktree and Reset All (Merge)')
385 ok_text = N_('Reset and Restore')
386 default = context.settings.get_value('reset::merge', 'ref', default='HEAD^')
387 ref = choose_ref(context, title, ok_text, default=default)
388 if ref:
389 cmds.do(cmds.ResetMerge, context, ref)
390 context.settings.set_value('reset::merge', 'ref', ref)
393 def reset_hard(context):
394 """Run "git reset --hard" to fully reset the working tree and staging area"""
395 title = N_('Restore Worktree and Reset All (Hard)')
396 ok_text = N_('Reset and Restore')
397 default = context.settings.get_value('reset::hard', 'ref', default='HEAD^')
398 ref = choose_ref(context, title, ok_text, default=default)
399 if ref:
400 cmds.do(cmds.ResetHard, context, ref)
401 context.settings.set_value('reset::hard', 'ref', ref)
404 def restore_worktree(context):
405 """Restore the worktree to the content from the specified commit"""
406 title = N_('Restore Worktree')
407 ok_text = N_('Restore Worktree')
408 default = context.settings.get_value('restore::worktree', 'ref', default='HEAD^')
409 ref = choose_ref(context, title, ok_text, default=default)
410 if ref:
411 cmds.do(cmds.RestoreWorktree, context, ref)
412 context.settings.set_value('restore::worktree', 'ref', ref)
415 def install():
416 """Install the GUI-model interaction hooks"""
417 Interaction.choose_ref = staticmethod(choose_ref)