CHANGES: document the vendored qtpy update
[git-cola.git] / cola / widgets / compare.py
blobd995403efa119b6b94e9f5a2598cdbfc33ab2e32
1 """Provides dialogs for comparing branches and commits."""
3 from qtpy import QtWidgets
4 from qtpy.QtCore import Qt
6 from .. import difftool
7 from .. import gitcmds
8 from .. import icons
9 from .. import qtutils
10 from ..i18n import N_
11 from ..qtutils import connect_button
12 from . import defs
13 from . import standard
16 class FileItem(QtWidgets.QTreeWidgetItem):
17 def __init__(self, path, icon):
18 QtWidgets.QTreeWidgetItem.__init__(self, [path])
19 self.path = path
20 self.setIcon(0, icon)
23 def compare_branches(context):
24 """Launches a dialog for comparing a pair of branches"""
25 view = CompareBranchesDialog(context, qtutils.active_window())
26 view.show()
27 return view
30 class CompareBranchesDialog(standard.Dialog):
31 def __init__(self, context, parent):
32 standard.Dialog.__init__(self, parent=parent)
34 self.context = context
35 self.BRANCH_POINT = N_('*** Branch Point ***')
36 self.SANDBOX = N_('*** Sandbox ***')
37 self.LOCAL = N_('Local')
38 self.diff_arg = ()
39 self.use_sandbox = False
40 self.start = None
41 self.end = None
43 self.setWindowTitle(N_('Branch Diff Viewer'))
45 self.remote_branches = gitcmds.branch_list(context, remote=True)
46 self.local_branches = gitcmds.branch_list(context, remote=False)
48 self.top_widget = QtWidgets.QWidget()
49 self.bottom_widget = QtWidgets.QWidget()
51 self.left_combo = QtWidgets.QComboBox()
52 self.left_combo.addItem(N_('Local'))
53 self.left_combo.addItem(N_('Remote'))
54 self.left_combo.setCurrentIndex(0)
56 self.right_combo = QtWidgets.QComboBox()
57 self.right_combo.addItem(N_('Local'))
58 self.right_combo.addItem(N_('Remote'))
59 self.right_combo.setCurrentIndex(1)
61 self.left_list = QtWidgets.QListWidget()
62 self.right_list = QtWidgets.QListWidget()
64 Expanding = QtWidgets.QSizePolicy.Expanding
65 Minimum = QtWidgets.QSizePolicy.Minimum
66 self.button_spacer = QtWidgets.QSpacerItem(1, 1, Expanding, Minimum)
68 self.button_compare = qtutils.create_button(
69 text=N_('Compare'), icon=icons.diff()
71 self.button_close = qtutils.close_button()
73 self.diff_files = standard.TreeWidget()
74 self.diff_files.headerItem().setText(0, N_('File Differences'))
76 self.top_grid_layout = qtutils.grid(
77 defs.no_margin,
78 defs.spacing,
79 (self.left_combo, 0, 0, 1, 1),
80 (self.left_list, 1, 0, 1, 1),
81 (self.right_combo, 0, 1, 1, 1),
82 (self.right_list, 1, 1, 1, 1),
84 self.top_widget.setLayout(self.top_grid_layout)
86 self.bottom_grid_layout = qtutils.grid(
87 defs.no_margin,
88 defs.button_spacing,
89 (self.diff_files, 0, 0, 1, 4),
90 (self.button_spacer, 1, 0, 1, 1),
91 (self.button_close, 1, 2, 1, 1),
92 (self.button_compare, 1, 3, 1, 1),
94 self.bottom_widget.setLayout(self.bottom_grid_layout)
96 self.splitter = qtutils.splitter(
97 Qt.Vertical, self.top_widget, self.bottom_widget
100 self.main_layout = qtutils.vbox(defs.margin, defs.spacing, self.splitter)
101 self.setLayout(self.main_layout)
103 connect_button(self.button_close, self.accept)
104 connect_button(self.button_compare, self.compare)
106 # pylint: disable=no-member
107 self.diff_files.itemDoubleClicked.connect(lambda _: self.compare())
108 self.left_combo.currentIndexChanged.connect(
109 lambda x: self.update_combo_boxes(left=True)
111 self.right_combo.currentIndexChanged.connect(
112 lambda x: self.update_combo_boxes(left=False)
115 self.left_list.itemSelectionChanged.connect(self.update_diff_files)
116 self.right_list.itemSelectionChanged.connect(self.update_diff_files)
118 self.update_combo_boxes(left=True)
119 self.update_combo_boxes(left=False)
121 # Pre-select the 0th elements
122 item = self.left_list.item(0)
123 if item:
124 self.left_list.setCurrentItem(item)
125 item.setSelected(True)
127 item = self.right_list.item(0)
128 if item:
129 self.right_list.setCurrentItem(item)
130 item.setSelected(True)
132 self.init_size(parent=parent)
134 def selection(self):
135 left_item = self.left_list.currentItem()
136 if left_item and left_item.isSelected():
137 left_item = left_item.text()
138 else:
139 left_item = None
140 right_item = self.right_list.currentItem()
141 if right_item and right_item.isSelected():
142 right_item = right_item.text()
143 else:
144 right_item = None
145 return (left_item, right_item)
147 def update_diff_files(self):
148 """Updates the list of files whenever the selection changes"""
149 # Left and Right refer to the comparison pair (l,r)
150 left_item, right_item = self.selection()
151 if not left_item or not right_item or left_item == right_item:
152 self.set_diff_files([])
153 return
154 left_item = self.remote_ref(left_item)
155 right_item = self.remote_ref(right_item)
157 # If any of the selection includes sandbox then we
158 # generate the same diff, regardless. This means we don't
159 # support reverse diffs against sandbox aka worktree.
160 if self.SANDBOX in (left_item, right_item):
161 self.use_sandbox = True
162 if left_item == self.SANDBOX:
163 self.diff_arg = (right_item,)
164 else:
165 self.diff_arg = (left_item,)
166 else:
167 self.diff_arg = (left_item, right_item)
168 self.use_sandbox = False
170 # start and end as in 'git diff start end'
171 self.start = left_item
172 self.end = right_item
174 context = self.context
175 if len(self.diff_arg) == 1:
176 files = gitcmds.diff_index_filenames(context, self.diff_arg[0])
177 else:
178 files = gitcmds.diff_filenames(context, *self.diff_arg)
180 self.set_diff_files(files)
182 def set_diff_files(self, files):
183 mk = FileItem
184 icon = icons.file_code()
185 self.diff_files.clear()
186 self.diff_files.addTopLevelItems([mk(f, icon) for f in files])
188 def remote_ref(self, branch):
189 """Returns the remote ref for 'git diff [local] [remote]'"""
190 context = self.context
191 if branch == self.BRANCH_POINT:
192 # Compare against the branch point so find the merge-base
193 branch = gitcmds.current_branch(context)
194 tracked_branch = gitcmds.tracked_branch(context)
195 if tracked_branch:
196 return gitcmds.merge_base(context, branch, tracked_branch)
197 remote_branches = gitcmds.branch_list(context, remote=True)
198 remote_branch = 'origin/%s' % branch
199 if remote_branch in remote_branches:
200 return gitcmds.merge_base(context, branch, remote_branch)
202 if 'origin/main' in remote_branches:
203 return gitcmds.merge_base(context, branch, 'origin/main')
205 if 'origin/master' in remote_branches:
206 return gitcmds.merge_base(context, branch, 'origin/master')
207 return 'HEAD'
208 # Compare against the remote branch
209 return branch
211 def update_combo_boxes(self, left=False):
212 """Update listwidgets from the combobox selection
214 Update either the left or right listwidgets
215 to reflect the available items.
217 if left:
218 which = self.left_combo.currentText()
219 widget = self.left_list
220 else:
221 which = self.right_combo.currentText()
222 widget = self.right_list
223 if not which:
224 return
225 # If we're looking at "local" stuff then provide the
226 # sandbox as a valid choice. If we're looking at
227 # "remote" stuff then also include the branch point.
228 if which == self.LOCAL:
229 new_list = [self.SANDBOX] + self.local_branches
230 else:
231 new_list = [self.BRANCH_POINT] + self.remote_branches
233 widget.clear()
234 widget.addItems(new_list)
235 if new_list:
236 item = widget.item(0)
237 widget.setCurrentItem(item)
238 item.setSelected(True)
240 def compare(self):
241 """Shows the diff for a specific file"""
242 tree_widget = self.diff_files
243 item = tree_widget.currentItem()
244 if item and item.isSelected():
245 self.compare_file(item.path)
247 def compare_file(self, filename):
248 """Initiates the difftool session"""
249 if self.use_sandbox:
250 left = self.diff_arg[0]
251 if len(self.diff_arg) > 1:
252 right = self.diff_arg[1]
253 else:
254 right = None
255 else:
256 left, right = self.start, self.end
257 context = self.context
258 difftool.difftool_launch(context, left=left, right=right, paths=[filename])