cmds: provide $DIRNAME in the environment for guitool commands
[git-cola.git] / cola / difftool.py
blobe5f6f74da36308c2ae3b8ace59a90940cc6790d1
1 from __future__ import division, absolute_import, unicode_literals
3 from qtpy import QtCore
4 from qtpy import QtWidgets
5 from qtpy.QtCore import Qt
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 .i18n import N_
14 from .interaction import Interaction
15 from .models import main
16 from .models import selection
17 from .widgets import completion
18 from .widgets import defs
19 from .widgets import filetree
22 def run():
23 """Start a default difftool session"""
24 files = selection.selected_group()
25 if not files:
26 return
27 s = selection.selection()
28 model = main.model()
29 launch_with_head(files, bool(s.staged), model.head)
32 def launch_with_head(filenames, staged, head):
33 """Launch difftool against the provided head"""
34 if head == 'HEAD':
35 left = None
36 else:
37 left = head
38 launch(left=left, staged=staged, paths=filenames)
41 def launch(left=None, right=None, paths=None, staged=False, dir_diff=False,
42 left_take_magic=False, left_take_parent=False):
43 """Launches 'git difftool' with given parameters
45 :param left: first argument to difftool
46 :param right: second argument to difftool_args
47 :param paths: paths to diff
48 :param staged: activate `git difftool --staged`
49 :param dir_diff: activate `git difftool --dir-diff`
50 :param left_take_magic: whether to append the magic ^! diff expression
51 :param left_take_parent: whether to append the first-parent ~ for diffing
53 """
55 difftool_args = ['git', 'difftool', '--no-prompt']
56 if staged:
57 difftool_args.append('--cached')
58 if dir_diff:
59 difftool_args.append('--dir-diff')
61 if left:
62 if left_take_parent or left_take_magic:
63 suffix = left_take_magic and '^!' or '~'
64 # Check root commit (no parents and thus cannot execute '~')
65 model = main.model()
66 git = model.git
67 status, out, err = git.rev_list(left, parents=True, n=1)
68 Interaction.log_status(status, out, err)
69 if status:
70 raise OSError('git rev-list command failed')
72 if len(out.split()) >= 2:
73 # Commit has a parent, so we can take its child as requested
74 left += suffix
75 else:
76 # No parent, assume it's the root commit, so we have to diff
77 # against the empty tree. Git's empty tree is a built-in
78 # constant object name.
79 left = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
80 if not right and left_take_magic:
81 right = left
82 difftool_args.append(left)
84 if right:
85 difftool_args.append(right)
87 if paths:
88 difftool_args.append('--')
89 difftool_args.extend(paths)
91 core.fork(difftool_args)
94 def diff_commits(parent, a, b):
95 """Show a dialog for diffing two commits"""
96 dlg = FileDiffDialog(parent, a=a, b=b)
97 dlg.show()
98 dlg.raise_()
99 return dlg.exec_() == QtWidgets.QDialog.Accepted
102 def diff_expression(parent, expr,
103 create_widget=False,
104 hide_expr=False,
105 focus_tree=False):
106 """Show a diff dialog for diff expressions"""
107 dlg = FileDiffDialog(parent,
108 expr=expr,
109 hide_expr=hide_expr,
110 focus_tree=focus_tree)
111 if create_widget:
112 return dlg
113 dlg.show()
114 dlg.raise_()
115 return dlg.exec_() == QtWidgets.QDialog.Accepted
118 class FileDiffDialog(QtWidgets.QDialog):
120 def __init__(self, parent, a=None, b=None, expr=None, title=None,
121 hide_expr=False, focus_tree=False):
122 """Show files with differences and launch difftool"""
124 QtWidgets.QDialog.__init__(self, parent)
125 self.setAttribute(Qt.WA_MacMetalStyle)
127 self.a = a
128 self.b = b
129 self.diff_expr = expr
131 if title is None:
132 title = N_('git-cola diff')
134 self.setWindowTitle(title)
135 self.setWindowModality(QtCore.Qt.WindowModal)
137 self.expr = completion.GitRefLineEdit(parent=self)
138 if expr is not None:
139 self.expr.setText(expr)
141 if expr is None or hide_expr:
142 self.expr.hide()
144 self.tree = filetree.FileTree(parent=self)
146 self.diff_button = qtutils.create_button(text=N_('Compare'),
147 icon=icons.diff(),
148 enabled=False)
149 self.diff_all_button = qtutils.create_button(text=N_('Compare All'),
150 icon=icons.diff())
151 self.close_button = qtutils.close_button()
153 self.button_layout = qtutils.hbox(defs.no_margin, defs.spacing,
154 qtutils.STRETCH,
155 self.diff_button,
156 self.diff_all_button,
157 defs.spacing,
158 self.close_button)
160 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
161 self.expr, self.tree,
162 self.button_layout)
163 self.setLayout(self.main_layout)
165 self.tree.itemSelectionChanged.connect(self.tree_selection_changed)
166 self.tree.itemDoubleClicked.connect(self.tree_double_clicked)
167 self.tree.up.connect(self.focus_input)
169 self.expr.textChanged.connect(self.text_changed)
171 self.expr.activated.connect(self.focus_tree)
172 self.expr.down.connect(self.focus_tree)
173 self.expr.enter.connect(self.focus_tree)
175 qtutils.connect_button(self.diff_button, self.diff)
176 qtutils.connect_button(self.diff_all_button,
177 lambda: self.diff(dir_diff=True))
178 qtutils.connect_button(self.close_button, self.close)
180 qtutils.add_action(self, 'Focus Input', self.focus_input, hotkeys.FOCUS)
181 qtutils.add_action(self, 'Diff All', lambda: self.diff(dir_diff=True),
182 hotkeys.CTRL_ENTER, hotkeys.CTRL_RETURN)
183 qtutils.add_close_action(self)
185 self.resize(720, 420)
186 self.refresh()
188 if focus_tree:
189 self.focus_tree()
191 def focus_tree(self):
192 """Focus the files tree"""
193 self.tree.setFocus()
195 def focus_input(self):
196 """Focus the expression input"""
197 self.expr.setFocus()
199 def text_changed(self, txt):
200 self.diff_expr = txt
201 self.refresh()
203 def refresh(self):
204 """Redo the diff when the expression changes"""
205 if self.diff_expr is not None:
206 self.diff_arg = utils.shell_split(self.diff_expr)
207 elif self.b is None:
208 self.diff_arg = [self.a]
209 else:
210 self.diff_arg = [self.a, self.b]
211 self.refresh_filenames()
213 def refresh_filenames(self):
214 if self.a and self.b is None:
215 filenames = gitcmds.diff_index_filenames(self.a)
216 else:
217 filenames = gitcmds.diff(self.diff_arg)
218 self.tree.set_filenames(filenames, select=True)
220 def tree_selection_changed(self):
221 has_selection = self.tree.has_selection()
222 self.diff_button.setEnabled(has_selection)
223 self.diff_all_button.setEnabled(has_selection)
225 def tree_double_clicked(self, item, column):
226 path = self.tree.filename_from_item(item)
227 left, right = self._left_right_args()
228 launch(left=left, right=right, paths=[path])
230 def diff(self, dir_diff=False):
231 paths = self.tree.selected_filenames()
232 left, right = self._left_right_args()
233 launch(left=left, right=right, paths=paths, dir_diff=dir_diff)
235 def _left_right_args(self):
236 if self.diff_arg:
237 left = self.diff_arg[0]
238 else:
239 left = None
240 if len(self.diff_arg) > 1:
241 right = self.diff_arg[1]
242 else:
243 right = None
244 return (left, right)