compare: use gitcmds.merge_base()
[git-cola.git] / cola / widgets / compare.py
blobbeadc19d4eb69345ccc90b69faeb851821a5c94b
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.i18n import N_
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):
33 def __init__(self, parent):
34 standard.Dialog.__init__(self, parent=parent)
36 self.BRANCH_POINT = N_('*** Branch Point ***')
37 self.SANDBOX = N_('*** Sandbox ***')
38 self.LOCAL = N_('Local')
40 self.remote_branches = gitcmds.branch_list(remote=True)
41 self.local_branches = gitcmds.branch_list(remote=False)
43 self.setWindowTitle(N_('Branch Diff Viewer'))
44 self.resize(658, 350)
46 self.main_layt = QtGui.QVBoxLayout(self)
47 self.main_layt.setMargin(defs.margin)
48 self.main_layt.setSpacing(defs.spacing)
50 self.splitter = QtGui.QSplitter(self)
51 self.splitter.setOrientation(QtCore.Qt.Vertical)
52 self.splitter.setHandleWidth(defs.handle_width)
54 self.top_widget = QtGui.QWidget(self.splitter)
56 self.top_grid_layt = QtGui.QGridLayout(self.top_widget)
57 self.top_grid_layt.setMargin(0)
58 self.top_grid_layt.setSpacing(defs.spacing)
60 self.left_combo = QtGui.QComboBox(self.top_widget)
61 self.left_combo.addItem(N_('Local'))
62 self.left_combo.addItem(N_('Remote'))
63 self.left_combo.setCurrentIndex(0)
64 self.top_grid_layt.addWidget(self.left_combo, 0, 0, 1, 1)
66 self.right_combo = QtGui.QComboBox(self.top_widget)
67 self.right_combo.addItem(N_('Local'))
68 self.right_combo.addItem(N_('Remote'))
69 self.right_combo.setCurrentIndex(1)
70 self.top_grid_layt.addWidget(self.right_combo, 0, 1, 1, 1)
72 self.left_list = QtGui.QListWidget(self.top_widget)
73 self.top_grid_layt.addWidget(self.left_list, 1, 0, 1, 1)
75 self.right_list = QtGui.QListWidget(self.top_widget)
76 self.top_grid_layt.addWidget(self.right_list, 1, 1, 1, 1)
78 self.bottom_widget = QtGui.QWidget(self.splitter)
79 self.bottom_grid_layt = QtGui.QGridLayout(self.bottom_widget)
80 self.bottom_grid_layt.setMargin(0)
81 self.bottom_grid_layt.setSpacing(defs.button_spacing)
83 self.button_spacer = QtGui.QSpacerItem(1, 1,
84 QtGui.QSizePolicy.Expanding,
85 QtGui.QSizePolicy.Minimum)
86 self.bottom_grid_layt.addItem(self.button_spacer, 1, 1, 1, 1)
88 self.button_compare = QtGui.QPushButton(self.bottom_widget)
89 self.button_compare.setText(N_('Compare'))
90 self.bottom_grid_layt.addWidget(self.button_compare, 1, 2, 1, 1)
92 self.button_close = QtGui.QPushButton(self.bottom_widget)
93 self.button_close.setText(N_('Close'))
94 self.bottom_grid_layt.addWidget(self.button_close, 1, 3, 1, 1)
96 self.diff_files = standard.TreeWidget(self.bottom_widget)
97 self.diff_files.headerItem().setText(0, N_('File Differences'))
99 self.bottom_grid_layt.addWidget(self.diff_files, 0, 0, 1, 4)
100 self.main_layt.addWidget(self.splitter)
102 connect_button(self.button_close, self.accept)
103 connect_button(self.button_compare, self.compare)
105 self.connect(self.diff_files,
106 SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
107 self.compare)
109 self.connect(self.left_combo,
110 SIGNAL('currentIndexChanged(int)'),
111 lambda x: self.update_combo_boxes(left=True))
113 self.connect(self.right_combo,
114 SIGNAL('currentIndexChanged(int)'),
115 lambda x: self.update_combo_boxes(left=False))
117 self.connect(self.left_list,
118 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
120 self.connect(self.right_list,
121 SIGNAL('itemSelectionChanged()'), self.update_diff_files)
123 self.update_combo_boxes(left=True)
124 self.update_combo_boxes(left=False)
126 # Pre-select the 0th elements
127 item = self.left_list.item(0)
128 if item:
129 self.left_list.setCurrentItem(item)
130 self.left_list.setItemSelected(item, True)
132 item = self.right_list.item(0)
133 if item:
134 self.right_list.setCurrentItem(item)
135 self.right_list.setItemSelected(item, True)
137 def selection(self):
138 left_item = self.left_list.currentItem()
139 if left_item and left_item.isSelected():
140 left_item = unicode(left_item.text())
141 else:
142 left_item = None
143 right_item = self.right_list.currentItem()
144 if right_item and right_item.isSelected():
145 right_item = unicode(right_item.text())
146 else:
147 right_item = None
148 return (left_item, right_item)
151 def update_diff_files(self, *rest):
152 """Updates the list of files whenever the selection changes"""
153 # Left and Right refer to the comparison pair (l,r)
154 left_item, right_item = self.selection()
155 if (not left_item or not right_item or
156 left_item == right_item):
157 self.set_diff_files([])
158 return
159 left_item = self.remote_ref(left_item)
160 right_item = self.remote_ref(right_item)
162 # If any of the selection includes sandbox then we
163 # generate the same diff, regardless. This means we don't
164 # support reverse diffs against sandbox aka worktree.
165 if self.SANDBOX in (left_item, right_item):
166 self.use_sandbox = True
167 if left_item == self.SANDBOX:
168 self.diff_arg = (right_item,)
169 else:
170 self.diff_arg = (left_item,)
171 else:
172 self.diff_arg = (left_item, right_item)
173 self.use_sandbox = False
175 # start and end as in 'git diff start end'
176 self.start = left_item
177 self.end = right_item
179 if len(self.diff_arg) == 1:
180 files = gitcmds.diff_index_filenames(self.diff_arg[0])
181 else:
182 files = gitcmds.diff_filenames(*self.diff_arg)
184 self.set_diff_files(files)
186 def set_diff_files(self, files):
187 mk = FileItem
188 icon = qtutils.icon('script.png')
189 self.diff_files.clear()
190 self.diff_files.addTopLevelItems([mk(f, icon) for f in files])
192 def remote_ref(self, branch):
193 """Returns the remote ref for 'git diff [local] [remote]'
195 if branch == self.BRANCH_POINT:
196 # Compare against the branch point so find the merge-base
197 branch = gitcmds.current_branch()
198 tracked_branch = gitcmds.tracked_branch()
199 if tracked_branch:
200 return gitcmds.merge_base(branch, tracked_branch)
201 else:
202 remote_branches = gitcmds.branch_list(remote=True)
203 remote_branch = 'origin/%s' % branch
204 if remote_branch in remote_branches:
205 return gitcmds.merge_base(branch, remote_branch)
207 elif 'origin/master' in remote_branches:
208 return gitcmds.merge_base(branch, 'origin/master')
209 else:
210 return 'HEAD'
211 else:
212 # Compare against the remote branch
213 return branch
216 def update_combo_boxes(self, left=False):
217 """Update listwidgets from the combobox selection
219 Update either the left or right listwidgets
220 to reflect the available items.
222 if left:
223 which = unicode(self.left_combo.currentText())
224 widget = self.left_list
225 else:
226 which = unicode(self.right_combo.currentText())
227 widget = self.right_list
228 if not which:
229 return
230 # If we're looking at "local" stuff then provide the
231 # sandbox as a valid choice. If we're looking at
232 # "remote" stuff then also include the branch point.
233 if which == self.LOCAL:
234 new_list = ([self.SANDBOX]+ self.local_branches)
235 else:
236 new_list = ([self.BRANCH_POINT] + self.remote_branches)
238 widget.clear()
239 widget.addItems(new_list)
240 if new_list:
241 item = widget.item(0)
242 widget.setCurrentItem(item)
243 widget.setItemSelected(item, True)
245 def compare(self, *args):
246 """Shows the diff for a specific file
248 tree_widget = self.diff_files
249 item = tree_widget.currentItem()
250 if item and item.isSelected():
251 self.compare_file(item.path)
253 def compare_file(self, filename):
254 """Initiates the difftool session"""
255 if self.use_sandbox:
256 arg = self.diff_arg
257 else:
258 arg = (self.start, self.end)
259 difftool.launch(arg + ('--', filename))