cola: Remove the 'cola.views' package
[git-cola.git] / cola / widgets / compare.py
blob09bf30833e838360ed9f1c4b253af905256cf7de
1 """Provides dialogs for comparing branches and commits."""
3 from PyQt4 import QtCore
4 from PyQt4 import QtGui
5 from PyQt4.QtCore import SIGNAL
7 from cola import qtutils
8 from cola import difftool
9 from cola import gitcmds
10 from cola.git import git
11 from cola.qtutils import connect_button
12 from cola.widgets import defs
13 from cola.widgets import standard
16 class FileItem(QtGui.QTreeWidgetItem):
17 def __init__(self, path, icon):
18 QtGui.QTreeWidgetItem.__init__(self, [path])
19 self.path = path
20 self.setIcon(0, icon)
23 def compare_branches():
24 """Launches a dialog for comparing a pair of branches"""
25 view = CompareBranchesDialog(qtutils.active_window())
26 view.show()
27 return view
30 class CompareBranchesDialog(standard.Dialog):
32 BRANCH_POINT = '*** Branch Point ***'
33 SANDBOX = '*** Sandbox ***'
34 LOCAL = 'Local'
36 def __init__(self, parent):
37 standard.Dialog.__init__(self, parent=parent)
38 self.remote_branches = gitcmds.branch_list(remote=True)
39 self.local_branches = gitcmds.branch_list(remote=False)
41 self.setWindowTitle(self.tr('Branch Diff Viewer'))
42 self.resize(658, 350)
44 self.main_layt = QtGui.QVBoxLayout(self)
45 self.main_layt.setMargin(defs.margin)
46 self.main_layt.setSpacing(defs.spacing)
48 self.splitter = QtGui.QSplitter(self)
49 self.splitter.setOrientation(QtCore.Qt.Vertical)
50 self.splitter.setHandleWidth(defs.handle_width)
52 self.top_widget = QtGui.QWidget(self.splitter)
54 self.top_grid_layt = QtGui.QGridLayout(self.top_widget)
55 self.top_grid_layt.setMargin(0)
56 self.top_grid_layt.setSpacing(defs.spacing)
58 self.left_combo = QtGui.QComboBox(self.top_widget)
59 self.left_combo.addItem(self.tr('Local'))
60 self.left_combo.addItem(self.tr('Remote'))
61 self.left_combo.setCurrentIndex(0)
62 self.top_grid_layt.addWidget(self.left_combo, 0, 0, 1, 1)
64 self.right_combo = QtGui.QComboBox(self.top_widget)
65 self.right_combo.addItem(self.tr('Local'))
66 self.right_combo.addItem(self.tr('Remote'))
67 self.right_combo.setCurrentIndex(1)
68 self.top_grid_layt.addWidget(self.right_combo, 0, 1, 1, 1)
70 self.left_list = QtGui.QListWidget(self.top_widget)
71 self.top_grid_layt.addWidget(self.left_list, 1, 0, 1, 1)
73 self.right_list = QtGui.QListWidget(self.top_widget)
74 self.top_grid_layt.addWidget(self.right_list, 1, 1, 1, 1)
76 self.bottom_widget = QtGui.QWidget(self.splitter)
77 self.bottom_grid_layt = QtGui.QGridLayout(self.bottom_widget)
78 self.bottom_grid_layt.setMargin(0)
79 self.bottom_grid_layt.setSpacing(defs.button_spacing)
81 self.button_spacer = QtGui.QSpacerItem(1, 1,
82 QtGui.QSizePolicy.Expanding,
83 QtGui.QSizePolicy.Minimum)
84 self.bottom_grid_layt.addItem(self.button_spacer, 1, 1, 1, 1)
86 self.button_compare = QtGui.QPushButton(self.bottom_widget)
87 self.button_compare.setText(self.tr('Compare'))
88 self.bottom_grid_layt.addWidget(self.button_compare, 1, 2, 1, 1)
90 self.button_close = QtGui.QPushButton(self.bottom_widget)
91 self.button_close.setText(self.tr('Close'))
92 self.bottom_grid_layt.addWidget(self.button_close, 1, 3, 1, 1)
94 self.diff_files = QtGui.QTreeWidget(self.bottom_widget)
95 self.diff_files.headerItem().setText(0, self.tr('File Differences'))
96 self.diff_files.setRootIsDecorated(False)
98 self.bottom_grid_layt.addWidget(self.diff_files, 0, 0, 1, 4)
99 self.main_layt.addWidget(self.splitter)
101 connect_button(self.button_close, self.accept)
102 connect_button(self.button_compare, self.compare)
104 self.connect(self.diff_files,
105 SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
106 self.compare)
108 self.connect(self.left_combo,
109 SIGNAL('currentIndexChanged(int)'),
110 lambda x: self.update_combo_boxes(left=True))
112 self.connect(self.right_combo,
113 SIGNAL('currentIndexChanged(int)'),
114 lambda x: self.update_combo_boxes(left=False))
116 self.connect(self.left_list,
117 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
119 self.connect(self.right_list,
120 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
122 self.update_combo_boxes(left=True)
123 self.update_combo_boxes(left=False)
125 # Pre-select the 0th elements
126 item = self.left_list.item(0)
127 if item:
128 self.left_list.setCurrentItem(item)
129 self.left_list.setItemSelected(item, True)
131 item = self.right_list.item(0)
132 if item:
133 self.right_list.setCurrentItem(item)
134 self.right_list.setItemSelected(item, True)
136 def selection(self):
137 left_item = self.left_list.currentItem()
138 if left_item and left_item.isSelected():
139 left_item = unicode(left_item.text())
140 else:
141 left_item = None
142 right_item = self.right_list.currentItem()
143 if right_item and right_item.isSelected():
144 right_item = unicode(right_item.text())
145 else:
146 right_item = None
147 return (left_item, right_item)
150 def update_diff_files(self, *rest):
151 """Updates the list of files whenever the selection changes"""
152 # Left and Right refer to the comparison pair (l,r)
153 left_item, right_item = self.selection()
154 if (not left_item or not right_item or
155 left_item == right_item):
156 self.set_diff_files([])
157 return
158 left_item = self.remote_ref(left_item)
159 right_item = self.remote_ref(right_item)
161 # If any of the selection includes sandbox then we
162 # generate the same diff, regardless. This means we don't
163 # support reverse diffs against sandbox aka worktree.
164 if self.SANDBOX in (left_item, right_item):
165 self.use_sandbox = True
166 if left_item == self.SANDBOX:
167 self.diff_arg = (right_item,)
168 else:
169 self.diff_arg = (left_item,)
170 else:
171 self.diff_arg = (left_item, right_item)
172 self.use_sandbox = False
174 # start and end as in 'git diff start end'
175 self.start = left_item
176 self.end = right_item
178 if len(self.diff_arg) == 1:
179 files = gitcmds.diff_index_filenames(self.diff_arg[0])
180 else:
181 files = gitcmds.diff_filenames(*self.diff_arg)
183 self.set_diff_files(files)
185 def set_diff_files(self, files):
186 mk = FileItem
187 icon = qtutils.icon('script.png')
188 self.diff_files.clear()
189 self.diff_files.addTopLevelItems([mk(f, icon) for f in files])
191 def remote_ref(self, branch):
192 """Returns the remote ref for 'git diff [local] [remote]'
194 if branch == self.BRANCH_POINT:
195 # Compare against the branch point so find the merge-base
196 branch = gitcmds.current_branch()
197 tracked_branch = gitcmds.tracked_branch()
198 if tracked_branch:
199 return git.merge_base(branch, tracked_branch)
200 else:
201 remote_branches = gitcmds.branch_list(remote=True)
202 remote_branch = 'origin/%s' % branch
203 if remote_branch in remote_branches:
204 return git.merge_base(branch, remote_branch)
206 elif 'origin/master' in remote_branches:
207 return git.merge_base(branch, 'origin/master')
208 else:
209 return 'HEAD'
210 else:
211 # Compare against the remote branch
212 return branch
215 def update_combo_boxes(self, left=False):
216 """Update listwidgets from the combobox selection
218 Update either the left or right listwidgets
219 to reflect the available items.
221 if left:
222 which = unicode(self.left_combo.currentText())
223 widget = self.left_list
224 else:
225 which = unicode(self.right_combo.currentText())
226 widget = self.right_list
227 if not which:
228 return
229 # If we're looking at "local" stuff then provide the
230 # sandbox as a valid choice. If we're looking at
231 # "remote" stuff then also include the branch point.
232 if which == self.LOCAL:
233 new_list = ([self.SANDBOX]+ self.local_branches)
234 else:
235 new_list = ([self.BRANCH_POINT] + self.remote_branches)
237 widget.clear()
238 widget.addItems(new_list)
239 if new_list:
240 item = widget.item(0)
241 widget.setCurrentItem(item)
242 widget.setItemSelected(item, True)
244 def compare(self, *args):
245 """Shows the diff for a specific file
247 tree_widget = self.diff_files
248 item = tree_widget.currentItem()
249 if item and item.isSelected():
250 self.compare_file(item.path)
252 def compare_file(self, filename):
253 """Initiates the difftool session"""
254 if self.use_sandbox:
255 arg = self.diff_arg
256 else:
257 arg = (self.start, self.end)
258 difftool.launch(arg + ('--', filename))