1 """Provides dialogs for comparing branches and commits."""
3 from qtpy
import QtWidgets
4 from qtpy
.QtCore
import Qt
6 from .. import difftool
11 from ..qtutils
import connect_button
13 from . import standard
16 class FileItem(QtWidgets
.QTreeWidgetItem
):
17 def __init__(self
, path
, icon
):
18 QtWidgets
.QTreeWidgetItem
.__init
__(self
, [path
])
23 def compare_branches(context
):
24 """Launches a dialog for comparing a pair of branches"""
25 view
= CompareBranchesDialog(context
, qtutils
.active_window())
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')
39 self
.use_sandbox
= False
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(
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(
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)
124 self
.left_list
.setCurrentItem(item
)
125 item
.setSelected(True)
127 item
= self
.right_list
.item(0)
129 self
.right_list
.setCurrentItem(item
)
130 item
.setSelected(True)
132 self
.init_size(parent
=parent
)
135 left_item
= self
.left_list
.currentItem()
136 if left_item
and left_item
.isSelected():
137 left_item
= left_item
.text()
140 right_item
= self
.right_list
.currentItem()
141 if right_item
and right_item
.isSelected():
142 right_item
= right_item
.text()
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([])
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
,)
165 self
.diff_arg
= (left_item
,)
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])
178 files
= gitcmds
.diff_filenames(context
, *self
.diff_arg
)
180 self
.set_diff_files(files
)
182 def set_diff_files(self
, files
):
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
)
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')
208 # Compare against the remote 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.
218 which
= self
.left_combo
.currentText()
219 widget
= self
.left_list
221 which
= self
.right_combo
.currentText()
222 widget
= self
.right_list
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
231 new_list
= [self
.BRANCH_POINT
] + self
.remote_branches
234 widget
.addItems(new_list
)
236 item
= widget
.item(0)
237 widget
.setCurrentItem(item
)
238 item
.setSelected(True)
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"""
250 left
= self
.diff_arg
[0]
251 if len(self
.diff_arg
) > 1:
252 right
= self
.diff_arg
[1]
256 left
, right
= self
.start
, self
.end
257 context
= self
.context
258 difftool
.difftool_launch(context
, left
=left
, right
=right
, paths
=[filename
])