requirements: install newer versions of send2trash
[git-cola.git] / cola / difftool.py
blob03123ff346570f4838e8596056514e1c7eab458b
1 import os
3 from qtpy import QtWidgets
4 from qtpy.QtCore import Qt
6 from . import cmds
7 from . import core
8 from . import gitcmds
9 from . import hotkeys
10 from . import icons
11 from . import qtutils
12 from . import utils
13 from .git import EMPTY_TREE_OID
14 from .i18n import N_
15 from .interaction import Interaction
16 from .widgets import completion
17 from .widgets import defs
18 from .widgets import filetree
19 from .widgets import standard
22 class LaunchDifftool(cmds.ContextCommand):
23 """Launch "git difftool" with the currently selected files"""
25 @staticmethod
26 def name():
27 return N_('Launch Diff Tool')
29 def do(self):
30 s = self.selection.selection()
31 if s.unmerged:
32 paths = s.unmerged
33 if utils.is_win32():
34 core.fork(['git', 'mergetool', '--no-prompt', '--'] + paths)
35 else:
36 cfg = self.cfg
37 cmd = cfg.terminal()
38 argv = utils.shell_split(cmd)
40 terminal = os.path.basename(argv[0])
41 shellquote_terms = {'xfce4-terminal'}
42 shellquote_default = terminal in shellquote_terms
44 mergetool = ['git', 'mergetool', '--no-prompt', '--']
45 mergetool.extend(paths)
46 needs_shellquote = cfg.get(
47 'cola.terminalshellquote', shellquote_default
50 if needs_shellquote:
51 argv.append(core.list2cmdline(mergetool))
52 else:
53 argv.extend(mergetool)
55 core.fork(argv)
56 else:
57 difftool_run(self.context)
60 class Difftool(standard.Dialog):
61 def __init__(
62 self,
63 context,
64 parent,
65 a=None,
66 b=None,
67 expr=None,
68 title=None,
69 hide_expr=False,
70 focus_tree=False,
72 """Show files with differences and launch difftool"""
74 standard.Dialog.__init__(self, parent=parent)
76 self.context = context
77 self.a = a
78 self.b = b
79 self.diff_expr = expr
81 if title is None:
82 title = N_('git-cola diff')
84 self.setWindowTitle(title)
85 self.setWindowModality(Qt.WindowModal)
87 self.expr = completion.GitRefLineEdit(context, parent=self)
88 if expr is not None:
89 self.expr.setText(expr)
91 if expr is None or hide_expr:
92 self.expr.hide()
94 self.tree = filetree.FileTree(parent=self)
96 self.diff_button = qtutils.create_button(
97 text=N_('Compare'), icon=icons.diff(), enabled=False, default=True
99 self.diff_button.setShortcut(hotkeys.DIFF)
101 self.diff_all_button = qtutils.create_button(
102 text=N_('Compare All'), icon=icons.diff()
104 self.edit_button = qtutils.edit_button()
105 self.edit_button.setShortcut(hotkeys.EDIT)
107 self.close_button = qtutils.close_button()
109 self.button_layout = qtutils.hbox(
110 defs.no_margin,
111 defs.spacing,
112 qtutils.STRETCH,
113 self.close_button,
114 self.edit_button,
115 self.diff_all_button,
116 self.diff_button,
119 self.main_layout = qtutils.vbox(
120 defs.margin, defs.spacing, self.expr, self.tree, self.button_layout
122 self.setLayout(self.main_layout)
124 # pylint: disable=no-member
125 self.tree.itemSelectionChanged.connect(self.tree_selection_changed)
126 self.tree.itemDoubleClicked.connect(self.tree_double_clicked)
127 self.tree.up.connect(self.focus_input)
129 self.expr.textChanged.connect(self.text_changed)
131 self.expr.activated.connect(self.focus_tree)
132 self.expr.down.connect(self.focus_tree)
133 self.expr.enter.connect(self.focus_tree)
135 qtutils.connect_button(self.diff_button, self.diff)
136 qtutils.connect_button(self.diff_all_button, lambda: self.diff(dir_diff=True))
137 qtutils.connect_button(self.edit_button, self.edit)
138 qtutils.connect_button(self.close_button, self.close)
140 qtutils.add_action(self, 'Focus Input', self.focus_input, hotkeys.FOCUS)
141 qtutils.add_action(
142 self,
143 'Diff All',
144 lambda: self.diff(dir_diff=True),
145 hotkeys.CTRL_ENTER,
146 hotkeys.CTRL_RETURN,
148 qtutils.add_close_action(self)
150 self.init_state(None, self.resize_widget, parent)
152 self.refresh()
153 if focus_tree:
154 self.focus_tree()
156 def resize_widget(self, parent):
157 """Set the initial size of the widget"""
158 width, height = qtutils.default_size(parent, 720, 420)
159 self.resize(width, height)
161 def focus_tree(self):
162 """Focus the files tree"""
163 self.tree.setFocus()
165 def focus_input(self):
166 """Focus the expression input"""
167 self.expr.setFocus()
169 def text_changed(self, txt):
170 self.diff_expr = txt
171 self.refresh()
173 def refresh(self):
174 """Redo the diff when the expression changes"""
175 if self.diff_expr is not None:
176 self.diff_arg = utils.shell_split(self.diff_expr)
177 elif self.b is None:
178 self.diff_arg = [self.a]
179 else:
180 self.diff_arg = [self.a, self.b]
181 self.refresh_filenames()
183 def refresh_filenames(self):
184 context = self.context
185 if self.a and self.b is None:
186 filenames = gitcmds.diff_index_filenames(context, self.a)
187 else:
188 filenames = gitcmds.diff(context, self.diff_arg)
189 self.tree.set_filenames(filenames, select=True)
191 def tree_selection_changed(self):
192 has_selection = self.tree.has_selection()
193 self.diff_button.setEnabled(has_selection)
194 self.diff_all_button.setEnabled(has_selection)
196 def tree_double_clicked(self, item, _column):
197 path = filetree.filename_from_item(item)
198 left, right = self._left_right_args()
199 difftool_launch(self.context, left=left, right=right, paths=[path])
201 def diff(self, dir_diff=False):
202 paths = self.tree.selected_filenames()
203 left, right = self._left_right_args()
204 difftool_launch(
205 self.context, left=left, right=right, paths=paths, dir_diff=dir_diff
208 def _left_right_args(self):
209 if self.diff_arg:
210 left = self.diff_arg[0]
211 else:
212 left = None
213 if len(self.diff_arg) > 1:
214 right = self.diff_arg[1]
215 else:
216 right = None
217 return (left, right)
219 def edit(self):
220 paths = self.tree.selected_filenames()
221 cmds.do(cmds.Edit, self.context, paths)
224 def diff_commits(context, parent, a, b):
225 """Show a dialog for diffing two commits"""
226 dlg = Difftool(context, parent, a=a, b=b)
227 dlg.show()
228 dlg.raise_()
229 return dlg.exec_() == QtWidgets.QDialog.Accepted
232 def diff_expression(
233 context, parent, expr, create_widget=False, hide_expr=False, focus_tree=False
235 """Show a diff dialog for diff expressions"""
236 dlg = Difftool(
237 context, parent, expr=expr, hide_expr=hide_expr, focus_tree=focus_tree
239 if create_widget:
240 return dlg
241 dlg.show()
242 dlg.raise_()
243 return dlg.exec_() == QtWidgets.QDialog.Accepted
246 def difftool_run(context):
247 """Start a default difftool session"""
248 selection = context.selection
249 files = selection.group()
250 if not files:
251 return
252 s = selection.selection()
253 head = context.model.head
254 difftool_launch_with_head(context, files, bool(s.staged), head)
257 def difftool_launch_with_head(context, filenames, staged, head):
258 """Launch difftool against the provided head"""
259 if head == 'HEAD':
260 left = None
261 else:
262 left = head
263 difftool_launch(context, left=left, staged=staged, paths=filenames)
266 def difftool_launch(
267 context,
268 left=None,
269 right=None,
270 paths=None,
271 staged=False,
272 dir_diff=False,
273 left_take_magic=False,
274 left_take_parent=False,
276 """Launches 'git difftool' with given parameters
278 :param left: first argument to difftool
279 :param right: second argument to difftool_args
280 :param paths: paths to diff
281 :param staged: activate `git difftool --staged`
282 :param dir_diff: activate `git difftool --dir-diff`
283 :param left_take_magic: whether to append the magic ^! diff expression
284 :param left_take_parent: whether to append the first-parent ~ for diffing
288 difftool_args = ['git', 'difftool', '--no-prompt']
289 if staged:
290 difftool_args.append('--cached')
291 if dir_diff:
292 difftool_args.append('--dir-diff')
294 if left:
295 if left_take_parent or left_take_magic:
296 suffix = '^!' if left_take_magic else '~'
297 # Check root commit (no parents and thus cannot execute '~')
298 git = context.git
299 status, out, err = git.rev_list(left, parents=True, n=1, _readonly=True)
300 Interaction.log_status(status, out, err)
301 if status:
302 raise OSError('git rev-list command failed')
304 if len(out.split()) >= 2:
305 # Commit has a parent, so we can take its child as requested
306 left += suffix
307 else:
308 # No parent, assume it's the root commit, so we have to diff
309 # against the empty tree.
310 left = EMPTY_TREE_OID
311 if not right and left_take_magic:
312 right = left
313 difftool_args.append(left)
315 if right:
316 difftool_args.append(right)
318 if paths:
319 difftool_args.append('--')
320 difftool_args.extend(paths)
322 runtask = context.runtask
323 if runtask:
324 Interaction.async_command(N_('Difftool'), difftool_args, runtask)
325 else:
326 core.fork(difftool_args)