git-cola v4.9.0
[git-cola.git] / cola / difftool.py
blobd579df5364852d0c8ade4afd186d040c2466d957
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 self.tree.itemSelectionChanged.connect(self.tree_selection_changed)
125 self.tree.itemDoubleClicked.connect(self.tree_double_clicked)
126 self.tree.up.connect(self.focus_input)
128 self.expr.textChanged.connect(self.text_changed)
130 self.expr.activated.connect(self.focus_tree)
131 self.expr.down.connect(self.focus_tree)
132 self.expr.enter.connect(self.focus_tree)
134 qtutils.connect_button(self.diff_button, self.diff)
135 qtutils.connect_button(self.diff_all_button, lambda: self.diff(dir_diff=True))
136 qtutils.connect_button(self.edit_button, self.edit)
137 qtutils.connect_button(self.close_button, self.close)
139 qtutils.add_action(self, 'Focus Input', self.focus_input, hotkeys.FOCUS)
140 qtutils.add_action(
141 self,
142 'Diff All',
143 lambda: self.diff(dir_diff=True),
144 hotkeys.CTRL_ENTER,
145 hotkeys.CTRL_RETURN,
147 qtutils.add_close_action(self)
149 self.init_state(None, self.resize_widget, parent)
151 self.refresh()
152 if focus_tree:
153 self.focus_tree()
155 def resize_widget(self, parent):
156 """Set the initial size of the widget"""
157 width, height = qtutils.default_size(parent, 720, 420)
158 self.resize(width, height)
160 def focus_tree(self):
161 """Focus the files tree"""
162 self.tree.setFocus()
164 def focus_input(self):
165 """Focus the expression input"""
166 self.expr.setFocus()
168 def text_changed(self, txt):
169 self.diff_expr = txt
170 self.refresh()
172 def refresh(self):
173 """Redo the diff when the expression changes"""
174 if self.diff_expr is not None:
175 self.diff_arg = utils.shell_split(self.diff_expr)
176 elif self.b is None:
177 self.diff_arg = [self.a]
178 else:
179 self.diff_arg = [self.a, self.b]
180 self.refresh_filenames()
182 def refresh_filenames(self):
183 context = self.context
184 if self.a and self.b is None:
185 filenames = gitcmds.diff_index_filenames(context, self.a)
186 else:
187 filenames = gitcmds.diff(context, self.diff_arg)
188 self.tree.set_filenames(filenames, select=True)
190 def tree_selection_changed(self):
191 has_selection = self.tree.has_selection()
192 self.diff_button.setEnabled(has_selection)
193 self.diff_all_button.setEnabled(has_selection)
195 def tree_double_clicked(self, item, _column):
196 path = filetree.filename_from_item(item)
197 left, right = self._left_right_args()
198 difftool_launch(self.context, left=left, right=right, paths=[path])
200 def diff(self, dir_diff=False):
201 paths = self.tree.selected_filenames()
202 left, right = self._left_right_args()
203 difftool_launch(
204 self.context, left=left, right=right, paths=paths, dir_diff=dir_diff
207 def _left_right_args(self):
208 if self.diff_arg:
209 left = self.diff_arg[0]
210 else:
211 left = None
212 if len(self.diff_arg) > 1:
213 right = self.diff_arg[1]
214 else:
215 right = None
216 return (left, right)
218 def edit(self):
219 paths = self.tree.selected_filenames()
220 cmds.do(cmds.Edit, self.context, paths)
223 def diff_commits(context, parent, a, b):
224 """Show a dialog for diffing two commits"""
225 dlg = Difftool(context, parent, a=a, b=b)
226 dlg.show()
227 dlg.raise_()
228 return dlg.exec_() == QtWidgets.QDialog.Accepted
231 def diff_expression(
232 context, parent, expr, create_widget=False, hide_expr=False, focus_tree=False
234 """Show a diff dialog for diff expressions"""
235 dlg = Difftool(
236 context, parent, expr=expr, hide_expr=hide_expr, focus_tree=focus_tree
238 if create_widget:
239 return dlg
240 dlg.show()
241 dlg.raise_()
242 return dlg.exec_() == QtWidgets.QDialog.Accepted
245 def difftool_run(context):
246 """Start a default difftool session"""
247 selection = context.selection
248 files = selection.group()
249 if not files:
250 return
251 s = selection.selection()
252 head = context.model.head
253 difftool_launch_with_head(context, files, bool(s.staged), head)
256 def difftool_launch_with_head(context, filenames, staged, head):
257 """Launch difftool against the provided head"""
258 if head == 'HEAD':
259 left = None
260 else:
261 left = head
262 difftool_launch(context, left=left, staged=staged, paths=filenames)
265 def difftool_launch(
266 context,
267 left=None,
268 right=None,
269 paths=None,
270 staged=False,
271 dir_diff=False,
272 left_take_magic=False,
273 left_take_parent=False,
275 """Launches 'git difftool' with given parameters
277 :param left: first argument to difftool
278 :param right: second argument to difftool_args
279 :param paths: paths to diff
280 :param staged: activate `git difftool --staged`
281 :param dir_diff: activate `git difftool --dir-diff`
282 :param left_take_magic: whether to append the magic "^!" diff expression
283 :param left_take_parent: whether to append the first-parent ~ for diffing
287 difftool_args = ['git', 'difftool', '--no-prompt']
288 if staged:
289 difftool_args.append('--cached')
290 if dir_diff:
291 difftool_args.append('--dir-diff')
293 if left:
294 if left_take_parent or left_take_magic:
295 suffix = '^!' if left_take_magic else '~'
296 # Check root commit (no parents and thus cannot execute '~')
297 git = context.git
298 status, out, err = git.rev_list(left, parents=True, n=1, _readonly=True)
299 Interaction.log_status(status, out, err)
300 if status:
301 raise OSError('git rev-list command failed')
303 if len(out.split()) >= 2:
304 # Commit has a parent, so we can take its child as requested
305 left += suffix
306 else:
307 # No parent, assume it's the root commit, so we have to diff
308 # against the empty tree.
309 left = EMPTY_TREE_OID
310 if not right and left_take_magic:
311 right = left
312 difftool_args.append(left)
314 if right:
315 difftool_args.append(right)
317 if paths:
318 difftool_args.append('--')
319 difftool_args.extend(paths)
321 runtask = context.runtask
322 if runtask:
323 Interaction.async_command(N_('Difftool'), difftool_args, runtask)
324 else:
325 core.fork(difftool_args)