widgets: refactor DAG diff widgets to widgets.diff
[git-cola.git] / cola / widgets / search.py
blobc0cadc8d6e0152206b5ddadbcc9e2913ecb8be92
1 """A widget for searching git commits"""
2 import os
3 import time
4 import subprocess
6 from PyQt4 import QtGui
7 from PyQt4 import QtCore
8 from PyQt4.QtCore import SIGNAL
10 from cola import gitcmds
11 from cola import utils
12 from cola import qtutils
13 from cola.i18n import N_
14 from cola.interaction import Interaction
15 from cola.git import git
16 from cola.qt import create_toolbutton
17 from cola.qtutils import connect_button
18 from cola.qtutils import dir_icon
19 from cola.widgets import defs
20 from cola.widgets import standard
21 from cola.widgets.diff import DiffTextEdit
24 def mkdate(timespec):
25 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
28 class SearchOptions(object):
29 def __init__(self):
30 self.query = ''
31 self.max_count = 500
32 self.start_date = ''
33 self.end_date = ''
36 class SearchWidget(standard.Dialog):
37 def __init__(self, parent):
38 standard.Dialog.__init__(self, parent)
39 self.setAttribute(QtCore.Qt.WA_MacMetalStyle)
40 self.setWindowTitle(N_('Search'))
42 self.mode_combo = QtGui.QComboBox()
43 self.browse_button = create_toolbutton(icon=dir_icon(),
44 tooltip=N_('Browse...'))
45 self.query = QtGui.QLineEdit()
47 self.start_date = QtGui.QDateEdit()
48 self.start_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
49 self.start_date.setCalendarPopup(True)
50 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
52 self.end_date = QtGui.QDateEdit()
53 self.end_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
54 self.end_date.setCalendarPopup(True)
55 self.end_date.setDisplayFormat(N_('yyyy-MM-dd'))
57 self.search_button = QtGui.QPushButton()
58 self.search_button.setText(N_('Search'))
59 self.search_button.setDefault(True)
61 self.max_count = QtGui.QSpinBox()
62 self.max_count.setMinimum(5)
63 self.max_count.setMaximum(9995)
64 self.max_count.setSingleStep(5)
65 self.max_count.setValue(500)
67 self.commit_list = QtGui.QListWidget()
68 self.commit_list.setMinimumSize(QtCore.QSize(1, 1))
69 self.commit_list.setAlternatingRowColors(True)
70 self.commit_list.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
72 self.commit_text = DiffTextEdit(self, whitespace=False)
74 self.button_export = QtGui.QPushButton()
75 self.button_export.setText(N_('Export Patches'))
77 self.button_cherrypick = QtGui.QPushButton()
78 self.button_cherrypick.setText(N_('Cherry Pick'))
80 self.button_close = QtGui.QPushButton()
81 self.button_close.setText(N_('Close'))
83 self.top_layout = QtGui.QHBoxLayout()
84 self.top_layout.setMargin(0)
85 self.top_layout.setSpacing(defs.button_spacing)
87 self.top_layout.addWidget(self.query)
88 self.top_layout.addWidget(self.start_date)
89 self.top_layout.addWidget(self.end_date)
90 self.top_layout.addWidget(self.browse_button)
91 self.top_layout.addWidget(self.search_button)
92 self.top_layout.addStretch()
93 self.top_layout.addWidget(self.mode_combo)
94 self.top_layout.addWidget(self.max_count)
96 self.splitter = QtGui.QSplitter()
97 self.splitter.setHandleWidth(defs.handle_width)
98 self.splitter.setOrientation(QtCore.Qt.Vertical)
99 self.splitter.setChildrenCollapsible(True)
100 self.splitter.addWidget(self.commit_list)
101 self.splitter.addWidget(self.commit_text)
103 self.bottom_layout = QtGui.QHBoxLayout()
104 self.bottom_layout.setMargin(0)
105 self.bottom_layout.setSpacing(defs.spacing)
106 self.bottom_layout.addWidget(self.button_export)
107 self.bottom_layout.addWidget(self.button_cherrypick)
108 self.bottom_layout.addStretch()
109 self.bottom_layout.addWidget(self.button_close)
111 self.main_layout = QtGui.QVBoxLayout()
112 self.main_layout.setMargin(defs.margin)
113 self.main_layout.setSpacing(defs.spacing)
114 self.main_layout.addLayout(self.top_layout)
115 self.main_layout.addWidget(self.splitter)
116 self.main_layout.addLayout(self.bottom_layout)
117 self.setLayout(self.main_layout)
119 if self.parent():
120 self.resize(self.parent().width(), self.parent().height())
121 else:
122 self.resize(720, 500)
125 def search():
126 """Return a callback to handle various search actions."""
127 return search_commits(qtutils.active_window())
130 class SearchEngine(object):
131 def __init__(self, model):
132 self.model = model
134 def rev_args(self):
135 max_count = self.model.max_count
136 return {
137 'no_color': True,
138 'max-count': max_count,
139 'pretty': 'format:%H %aN - %s - %ar',
142 def common_args(self):
143 return (self.model.query, self.rev_args())
145 def search(self):
146 if not self.validate():
147 return
148 return self.results()
150 def validate(self):
151 return len(self.model.query) > 1
153 def revisions(self, *args, **kwargs):
154 revlist = git.log(*args, **kwargs)
155 return gitcmds.parse_rev_list(revlist)
157 def results(self):
158 pass
160 class RevisionSearch(SearchEngine):
161 def results(self):
162 query, opts = self.common_args()
163 args = utils.shell_split(query)
164 return self.revisions(all=True, *args, **opts)
167 class PathSearch(SearchEngine):
168 def results(self):
169 query, args = self.common_args()
170 paths = ['--'] + utils.shell_split(query)
171 return self.revisions(all=True, *paths, **args)
174 class MessageSearch(SearchEngine):
175 def results(self):
176 query, kwargs = self.common_args()
177 return self.revisions(all=True, grep=query, **kwargs)
180 class AuthorSearch(SearchEngine):
181 def results(self):
182 query, kwargs = self.common_args()
183 return self.revisions(all=True, author=query, **kwargs)
186 class CommitterSearch(SearchEngine):
187 def results(self):
188 query, kwargs = self.common_args()
189 return self.revisions(all=True, committer=query, **kwargs)
192 class DiffSearch(SearchEngine):
193 def results(self):
194 query, kwargs = self.common_args()
195 return gitcmds.parse_rev_list(
196 git.log('-S'+query, all=True, **kwargs))
199 class DateRangeSearch(SearchEngine):
200 def validate(self):
201 return self.model.start_date < self.model.end_date
203 def results(self):
204 kwargs = self.rev_args()
205 start_date = self.model.start_date
206 end_date = self.model.end_date
207 return self.revisions(date='iso',
208 all=True,
209 after=start_date,
210 before=end_date,
211 **kwargs)
214 class Search(SearchWidget):
216 def __init__(self, model, parent):
217 SearchWidget.__init__(self, parent)
218 self.model = model
220 self.EXPR = N_('Search by Expression')
221 self.PATH = N_('Search by Path')
222 self.MESSAGE = N_('Search Commit Messages')
223 self.DIFF = N_('Search Diffs')
224 self.AUTHOR = N_('Search Authors')
225 self.COMMITTER = N_('Search Committers')
226 self.DATE_RANGE = N_('Search Date Range')
228 # Each search type is handled by a distinct SearchEngine subclass
229 self.engines = {
230 self.EXPR: RevisionSearch,
231 self.PATH: PathSearch,
232 self.MESSAGE: MessageSearch,
233 self.DIFF: DiffSearch,
234 self.AUTHOR: AuthorSearch,
235 self.COMMITTER: CommitterSearch,
236 self.DATE_RANGE: DateRangeSearch,
239 self.modes = (self.EXPR, self.PATH, self.DATE_RANGE,
240 self.DIFF, self.MESSAGE, self.AUTHOR, self.COMMITTER)
241 self.mode_combo.addItems(self.modes)
243 connect_button(self.search_button, self.search_callback)
244 connect_button(self.browse_button, self.browse_callback)
245 connect_button(self.button_export, self.export_patch)
246 connect_button(self.button_cherrypick, self.cherry_pick)
247 connect_button(self.button_close, self.accept)
249 self.connect(self.mode_combo, SIGNAL('currentIndexChanged(int)'),
250 self.mode_index_changed)
252 self.connect(self.commit_list,
253 SIGNAL('itemSelectionChanged()'),
254 self.display)
256 self.set_start_date(mkdate(time.time()-(87640*31)))
257 self.set_end_date(mkdate(time.time()+87640))
258 self.set_mode(self.EXPR)
260 self.query.setFocus()
262 def mode_index_changed(self, idx):
263 mode = self.mode()
264 self.update_shown_widgets(mode)
265 if mode == self.PATH:
266 self.browse_callback()
268 def set_commit_list(self, commits):
269 widget = self.commit_list
270 widget.clear()
271 widget.addItems(commits)
273 def set_start_date(self, datestr):
274 self.set_date(self.start_date, datestr)
276 def set_end_date(self, datestr):
277 self.set_date(self.end_date, datestr)
279 def set_date(self, widget, datestr):
280 fmt = QtCore.Qt.ISODate
281 date = QtCore.QDate.fromString(datestr, fmt)
282 if date:
283 widget.setDate(date)
285 def set_mode(self, mode):
286 idx = self.modes.index(mode)
287 self.mode_combo.setCurrentIndex(idx)
288 self.update_shown_widgets(mode)
290 def update_shown_widgets(self, mode):
291 date_shown = mode == self.DATE_RANGE
292 browse_shown = mode == self.PATH
293 self.query.setVisible(not date_shown)
294 self.browse_button.setVisible(browse_shown)
295 self.start_date.setVisible(date_shown)
296 self.end_date.setVisible(date_shown)
298 def mode(self):
299 return str(self.mode_combo.currentText())
301 def search_callback(self, *args):
302 engineclass = self.engines[self.mode()]
303 self.model.query = unicode(self.query.text())
304 self.model.max_count = self.max_count.value()
306 fmt = QtCore.Qt.ISODate
307 self.model.start_date = str(self.start_date.date().toString(fmt))
308 self.model.end_date = str(self.end_date.date().toString(fmt))
310 self.results = engineclass(self.model).search()
311 if self.results:
312 self.display_results()
313 else:
314 self.commit_list.clear()
315 self.commit_text.setText('')
317 def browse_callback(self):
318 paths = QtGui.QFileDialog.getOpenFileNames(self,
319 N_('Choose Path(s)'))
320 if not paths:
321 return
322 filepaths = []
323 lenprefix = len(os.getcwd()) + 1
324 for path in map(lambda x: unicode(x), paths):
325 if not path.startswith(os.getcwd()):
326 continue
327 filepaths.append(path[lenprefix:])
328 query = subprocess.list2cmdline(filepaths)
329 self.query.setText(query)
330 if query:
331 self.search_callback()
333 def display_results(self):
334 commit_list = map(lambda x: x[1], self.results)
335 self.set_commit_list(commit_list)
337 def display(self, *args):
338 widget = self.commit_list
339 row, selected = qtutils.selected_row(widget)
340 if not selected or len(self.results) < row:
341 self.commit_text.setText('')
342 return
343 revision = self.results[row][0]
344 qtutils.set_clipboard(revision)
345 diff = gitcmds.commit_diff(revision)
346 self.commit_text.setText(diff)
348 def export_patch(self):
349 widget = self.commit_list
350 row, selected = qtutils.selected_row(widget)
351 if not selected or len(self.results) < row:
352 return
353 revision = self.results[row][0]
354 Interaction.log_status(*self.model.export_patchset(revision, revision))
356 def cherry_pick(self):
357 widget = self.commit_list
358 row, selected = qtutils.selected_row(widget)
359 if not selected or len(self.results) < row:
360 return
361 revision = self.results[row][0]
362 Interaction.log_status(*git.cherry_pick(revision,
363 with_stderr=True,
364 with_status=True))
366 def search_commits(parent):
367 opts = SearchOptions()
368 widget = Search(opts, parent)
369 widget.show()
370 return widget
374 if __name__ == '__main__':
375 import sys
376 app = QtGui.QApplication(sys.argv)
377 search = Search()
378 search.show()
379 sys.exit(app.exec_())