Merge pull request #1379 from github/dependabot/github_actions/styfle/cancel-workflow...
[git-cola.git] / cola / guicmds.py
blobfac06eebc0509dd8f8ca0278a6f979f3c548ae17
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 delete_branch(context):
23 """Launch the 'Delete Branch' dialog."""
24 icon = icons.discard()
25 branch = choose_branch(context, N_('Delete Branch'), N_('Delete'), icon=icon)
26 if not branch:
27 return
28 cmds.do(cmds.DeleteBranch, context, branch)
31 def delete_remote_branch(context):
32 """Launch the 'Delete Remote Branch' dialog."""
33 remote_branch = choose_remote_branch(
34 context, N_('Delete Remote Branch'), N_('Delete'), icon=icons.discard()
36 if not remote_branch:
37 return
38 remote, branch = gitcmds.parse_remote_branch(remote_branch)
39 if remote and branch:
40 cmds.do(cmds.DeleteRemoteBranch, context, remote, branch)
43 def browse_current(context):
44 """Launch the 'Browse Current Branch' dialog."""
45 branch = gitcmds.current_branch(context)
46 BrowseBranch.browse(context, branch)
49 def browse_other(context):
50 """Prompt for a branch and inspect content at that point in time."""
51 # Prompt for a branch to browse
52 branch = choose_ref(context, N_('Browse Commits...'), N_('Browse'))
53 if not branch:
54 return
55 BrowseBranch.browse(context, branch)
58 def checkout_branch(context, default=None):
59 """Launch the 'Checkout Branch' dialog."""
60 branch = choose_potential_branch(
61 context, N_('Checkout Branch'), N_('Checkout'), default=default
63 if not branch:
64 return
65 cmds.do(cmds.CheckoutBranch, context, branch)
68 def cherry_pick(context):
69 """Launch the 'Cherry-Pick' dialog."""
70 revs, summaries = gitcmds.log_helper(context, all=True)
71 commits = select_commits(
72 context, N_('Cherry-Pick Commit'), revs, summaries, multiselect=False
74 if not commits:
75 return
76 cmds.do(cmds.CherryPick, context, commits)
79 def new_repo(context):
80 """Prompt for a new directory and create a new Git repository
82 :returns str: repository path or None if no repository was created.
84 """
85 git = context.git
86 path = qtutils.opendir_dialog(N_('New Repository...'), core.getcwd())
87 if not path:
88 return None
89 # Avoid needlessly calling `git init`.
90 if git.is_git_repository(path):
91 # We could prompt here and confirm that they really didn't
92 # mean to open an existing repository, but I think
93 # treating it like an "Open" is a sensible DWIM answer.
94 return path
96 status, out, err = git.init(path)
97 if status == 0:
98 return path
100 title = N_('Error Creating Repository')
101 Interaction.command_error(title, 'git init', status, out, err)
102 return None
105 def open_new_repo(context):
106 """Create a new repository and open it"""
107 dirname = new_repo(context)
108 if not dirname:
109 return
110 cmds.do(cmds.OpenRepo, context, dirname)
113 def new_bare_repo(context):
114 """Create a bare repository and configure a remote pointing to it"""
115 result = None
116 repo = prompt_for_new_bare_repo()
117 if not repo:
118 return result
119 # Create bare repo
120 ok = cmds.do(cmds.NewBareRepo, context, repo)
121 if not ok:
122 return result
123 # Add a new remote pointing to the bare repo
124 parent = qtutils.active_window()
125 add_remote = editremotes.add_remote(
126 context, parent, name=os.path.basename(repo), url=repo, readonly_url=True
128 if add_remote:
129 result = repo
131 return result
134 def prompt_for_new_bare_repo():
135 """Prompt for a directory and name for a new bare repository"""
136 path = qtutils.opendir_dialog(N_('Select Directory...'), core.getcwd())
137 if not path:
138 return None
140 bare_repo = None
141 default = os.path.basename(core.getcwd())
142 if not default.endswith('.git'):
143 default += '.git'
144 while not bare_repo:
145 name, ok = qtutils.prompt(
146 N_('Enter a name for the new bare repo'),
147 title=N_('New Bare Repository...'),
148 text=default,
150 if not name or not ok:
151 return None
152 if not name.endswith('.git'):
153 name += '.git'
154 repo = os.path.join(path, name)
155 if core.isdir(repo):
156 Interaction.critical(N_('Error'), N_('"%s" already exists') % repo)
157 else:
158 bare_repo = repo
160 return bare_repo
163 def export_patches(context):
164 """Run 'git format-patch' on a list of commits."""
165 revs, summaries = gitcmds.log_helper(context)
166 to_export_and_output = select_commits_and_output(
167 context, N_('Export Patches'), revs, summaries
169 if not to_export_and_output['to_export']:
170 return
172 cmds.do(
173 cmds.FormatPatch,
174 context,
175 reversed(to_export_and_output['to_export']),
176 reversed(revs),
177 output=to_export_and_output['output'],
181 def diff_against_commit(context):
182 """Diff against any commit and checkout changes using the Diff Editor"""
183 icon = icons.compare()
184 ref = choose_ref(context, N_('Diff Against Commit'), N_('Diff'), icon=icon)
185 if not ref:
186 return
187 cmds.do(cmds.DiffAgainstCommitMode, context, ref)
190 def diff_expression(context):
191 """Diff using an arbitrary expression."""
192 tracked = gitcmds.tracked_branch(context)
193 current = gitcmds.current_branch(context)
194 if tracked and current:
195 ref = tracked + '..' + current
196 else:
197 ref = '@{upstream}..'
198 difftool.diff_expression(context, qtutils.active_window(), ref)
201 def open_repo(context):
202 """Open a repository in the current window"""
203 model = context.model
204 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
205 if not dirname:
206 return
207 cmds.do(cmds.OpenRepo, context, dirname)
210 def open_repo_in_new_window(context):
211 """Spawn a new cola session."""
212 model = context.model
213 dirname = qtutils.opendir_dialog(N_('Open Git Repository'), model.getcwd())
214 if not dirname:
215 return
216 cmds.do(cmds.OpenNewRepo, context, dirname)
219 def open_quick_repo_search(context, parent=None):
220 """Open a Quick Repository Search dialog"""
221 if parent is None:
222 parent = qtutils.active_window()
223 settings = context.settings
224 items = settings.bookmarks + settings.recent
226 if items:
227 cfg = context.cfg
228 default_repo = cfg.get('cola.defaultrepo')
230 entries = QtGui.QStandardItemModel()
231 added = set()
232 normalize = display.normalize_path
233 star_icon = icons.star()
234 folder_icon = icons.folder()
236 for item in items:
237 key = normalize(item['path'])
238 if key in added:
239 continue
241 name = item['name']
242 if default_repo == item['path']:
243 icon = star_icon
244 else:
245 icon = folder_icon
247 entry = switcher.switcher_item(key, icon, name)
248 entries.appendRow(entry)
249 added.add(key)
251 title = N_('Quick Open Repository')
252 place_holder = N_('Search repositories by name...')
253 switcher.switcher_inner_view(
254 context,
255 entries,
256 title,
257 place_holder=place_holder,
258 enter_action=lambda entry: cmds.do(cmds.OpenRepo, context, entry.key),
259 parent=parent,
263 def load_commitmsg(context):
264 """Load a commit message from a file."""
265 model = context.model
266 filename = qtutils.open_file(N_('Load Commit Message'), directory=model.getcwd())
267 if filename:
268 cmds.do(cmds.LoadCommitMessageFromFile, context, filename)
271 def choose_from_dialog(get, context, title, button_text, default, icon=None):
272 """Choose a value from a dialog using the `get` method"""
273 parent = qtutils.active_window()
274 return get(context, title, button_text, parent, default=default, icon=icon)
277 def choose_ref(context, title, button_text, default=None, icon=None):
278 """Choose a Git ref and return it"""
279 return choose_from_dialog(
280 completion.GitRefDialog.get, context, title, button_text, default, icon=icon
284 def choose_branch(context, title, button_text, default=None, icon=None):
285 """Choose a branch and return either the chosen branch or an empty value"""
286 return choose_from_dialog(
287 completion.GitBranchDialog.get, context, title, button_text, default, icon=icon
291 def choose_potential_branch(context, title, button_text, default=None, icon=None):
292 """Choose a "potential" branch for checking out.
294 This dialog includes remote branches from which new local branches can be created.
296 return choose_from_dialog(
297 completion.GitCheckoutBranchDialog.get,
298 context,
299 title,
300 button_text,
301 default,
302 icon=icon,
306 def choose_remote_branch(context, title, button_text, default=None, icon=None):
307 """Choose a remote branch"""
308 return choose_from_dialog(
309 completion.GitRemoteBranchDialog.get,
310 context,
311 title,
312 button_text,
313 default,
314 icon=icon,
318 def review_branch(context):
319 """Diff against an arbitrary revision, branch, tag, etc."""
320 branch = choose_ref(context, N_('Select Branch to Review'), N_('Review'))
321 if not branch:
322 return
323 merge_base = gitcmds.merge_base_parent(context, branch)
324 difftool.diff_commits(context, qtutils.active_window(), merge_base, branch)
327 def rename_branch(context):
328 """Launch the 'Rename Branch' dialogs."""
329 branch = choose_branch(context, N_('Rename Existing Branch'), N_('Select'))
330 if not branch:
331 return
332 new_branch = choose_branch(context, N_('Enter New Branch Name'), N_('Rename'))
333 if not new_branch:
334 return
335 cmds.do(cmds.RenameBranch, context, branch, new_branch)
338 def reset_soft(context):
339 """Run "git reset --soft" to reset the branch HEAD"""
340 title = N_('Reset Branch (Soft)')
341 ok_text = N_('Reset Branch')
342 ref = choose_ref(context, title, ok_text, default='HEAD^')
343 if ref:
344 cmds.do(cmds.ResetSoft, context, ref)
347 def reset_mixed(context):
348 """Run "git reset --mixed" to reset the branch HEAD and staging area"""
349 title = N_('Reset Branch and Stage (Mixed)')
350 ok_text = N_('Reset')
351 ref = choose_ref(context, title, ok_text, default='HEAD^')
352 if ref:
353 cmds.do(cmds.ResetMixed, context, ref)
356 def reset_keep(context):
357 """Run "git reset --keep" safe reset to avoid clobbering local changes"""
358 title = N_('Reset All (Keep Unstaged Changes)')
359 ref = choose_ref(context, title, N_('Reset and Restore'))
360 if ref:
361 cmds.do(cmds.ResetKeep, context, ref)
364 def reset_merge(context):
365 """Run "git reset --merge" to reset the working tree and staging area
367 The staging area is allowed to carry forward unmerged index entries,
368 but if any unstaged changes would be clobbered by the reset then the
369 reset is aborted.
371 title = N_('Restore Worktree and Reset All (Merge)')
372 ok_text = N_('Reset and Restore')
373 ref = choose_ref(context, title, ok_text, default='HEAD^')
374 if ref:
375 cmds.do(cmds.ResetMerge, context, ref)
378 def reset_hard(context):
379 """Run "git reset --hard" to fully reset the working tree and staging area"""
380 title = N_('Restore Worktree and Reset All (Hard)')
381 ok_text = N_('Reset and Restore')
382 ref = choose_ref(context, title, ok_text, default='HEAD^')
383 if ref:
384 cmds.do(cmds.ResetHard, context, ref)
387 def restore_worktree(context):
388 """Restore the worktree to the content from the specified commit"""
389 title = N_('Restore Worktree')
390 ok_text = N_('Restore Worktree')
391 ref = choose_ref(context, title, ok_text, default='HEAD^')
392 if ref:
393 cmds.do(cmds.RestoreWorktree, context, ref)
396 def install():
397 """Install the GUI-model interaction hooks"""
398 Interaction.choose_ref = staticmethod(choose_ref)