search: small perf and robustness tweaks
[git-cola.git] / cola / widgets / search.py
blob51191ae8ae6fb1b12c824a3908e74f15d3aaacdb
1 """A widget for searching git commits"""
2 from __future__ import division, absolute_import, unicode_literals
4 import time
6 from PyQt4 import QtGui
7 from PyQt4 import QtCore
8 from PyQt4.QtCore import Qt
9 from PyQt4.QtCore import SIGNAL
11 from cola import core
12 from cola import gitcmds
13 from cola import icons
14 from cola import utils
15 from cola import qtutils
16 from cola.i18n import N_
17 from cola.interaction import Interaction
18 from cola.git import git
19 from cola.git import STDOUT
20 from cola.qtutils import connect_button
21 from cola.qtutils import create_toolbutton
22 from cola.widgets import defs
23 from cola.widgets import standard
24 from cola.widgets.diff import DiffTextEdit
27 def mkdate(timespec):
28 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
31 class SearchOptions(object):
33 def __init__(self):
34 self.query = ''
35 self.max_count = 500
36 self.start_date = ''
37 self.end_date = ''
40 class SearchWidget(standard.Dialog):
42 def __init__(self, parent):
43 standard.Dialog.__init__(self, parent)
44 self.setAttribute(Qt.WA_MacMetalStyle)
45 self.setWindowTitle(N_('Search'))
47 self.mode_combo = QtGui.QComboBox()
48 self.browse_button = create_toolbutton(icon=icons.folder(),
49 tooltip=N_('Browse...'))
50 self.query = QtGui.QLineEdit()
52 self.start_date = QtGui.QDateEdit()
53 self.start_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
54 self.start_date.setCalendarPopup(True)
55 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
57 self.end_date = QtGui.QDateEdit()
58 self.end_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
59 self.end_date.setCalendarPopup(True)
60 self.end_date.setDisplayFormat(N_('yyyy-MM-dd'))
62 icon = icons.search()
63 self.search_button = qtutils.create_button(text=N_('Search'),
64 icon=icon, default=True)
66 self.max_count = QtGui.QSpinBox()
67 self.max_count.setMinimum(5)
68 self.max_count.setMaximum(9995)
69 self.max_count.setSingleStep(5)
70 self.max_count.setValue(500)
72 self.commit_list = QtGui.QListWidget()
73 self.commit_list.setMinimumSize(QtCore.QSize(1, 1))
74 self.commit_list.setAlternatingRowColors(True)
75 selection_mode = QtGui.QAbstractItemView.SingleSelection
76 self.commit_list.setSelectionMode(selection_mode)
78 self.commit_text = DiffTextEdit(self, whitespace=False)
80 self.button_export = qtutils.create_button(text=N_('Export Patches'),
81 icon=icons.diff())
83 self.button_cherrypick = qtutils.create_button(text=N_('Cherry Pick'),
84 icon=icons.save())
85 self.button_close = qtutils.close_button()
87 self.top_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
88 self.query, self.start_date,
89 self.end_date, self.browse_button,
90 self.search_button, qtutils.STRETCH,
91 self.mode_combo, self.max_count)
93 self.splitter = qtutils.splitter(Qt.Vertical,
94 self.commit_list, self.commit_text)
96 self.bottom_layout = qtutils.hbox(defs.no_margin, defs.spacing,
97 self.button_export,
98 self.button_cherrypick,
99 qtutils.STRETCH, self.button_close)
101 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
102 self.top_layout, self.splitter,
103 self.bottom_layout)
104 self.setLayout(self.main_layout)
106 if self.parent():
107 self.resize(self.parent().width(), self.parent().height())
108 else:
109 self.resize(720, 500)
112 def search():
113 """Return a callback to handle various search actions."""
114 return search_commits(qtutils.active_window())
117 class SearchEngine(object):
118 def __init__(self, model):
119 self.model = model
121 def rev_args(self):
122 max_count = self.model.max_count
123 return {
124 'no_color': True,
125 'max-count': max_count,
126 'pretty': 'format:%H %aN - %s - %ar',
129 def common_args(self):
130 return (self.model.query, self.rev_args())
132 def search(self):
133 if not self.validate():
134 return
135 return self.results()
137 def validate(self):
138 return len(self.model.query) > 1
140 def revisions(self, *args, **kwargs):
141 revlist = git.log(*args, **kwargs)[STDOUT]
142 return gitcmds.parse_rev_list(revlist)
144 def results(self):
145 pass
148 class RevisionSearch(SearchEngine):
150 def results(self):
151 query, opts = self.common_args()
152 args = utils.shell_split(query)
153 return self.revisions(all=True, *args, **opts)
156 class PathSearch(SearchEngine):
158 def results(self):
159 query, args = self.common_args()
160 paths = ['--'] + utils.shell_split(query)
161 return self.revisions(all=True, *paths, **args)
164 class MessageSearch(SearchEngine):
166 def results(self):
167 query, kwargs = self.common_args()
168 return self.revisions(all=True, grep=query, **kwargs)
171 class AuthorSearch(SearchEngine):
173 def results(self):
174 query, kwargs = self.common_args()
175 return self.revisions(all=True, author=query, **kwargs)
178 class CommitterSearch(SearchEngine):
180 def results(self):
181 query, kwargs = self.common_args()
182 return self.revisions(all=True, committer=query, **kwargs)
185 class DiffSearch(SearchEngine):
187 def results(self):
188 query, kwargs = self.common_args()
189 return gitcmds.parse_rev_list(
190 git.log('-S'+query, all=True, **kwargs)[STDOUT])
193 class DateRangeSearch(SearchEngine):
195 def validate(self):
196 return self.model.start_date < self.model.end_date
198 def results(self):
199 kwargs = self.rev_args()
200 start_date = self.model.start_date
201 end_date = self.model.end_date
202 return self.revisions(date='iso',
203 all=True,
204 after=start_date,
205 before=end_date,
206 **kwargs)
209 class Search(SearchWidget):
211 def __init__(self, model, parent):
212 SearchWidget.__init__(self, parent)
213 self.model = model
215 self.EXPR = N_('Search by Expression')
216 self.PATH = N_('Search by Path')
217 self.MESSAGE = N_('Search Commit Messages')
218 self.DIFF = N_('Search Diffs')
219 self.AUTHOR = N_('Search Authors')
220 self.COMMITTER = N_('Search Committers')
221 self.DATE_RANGE = N_('Search Date Range')
223 # Each search type is handled by a distinct SearchEngine subclass
224 self.engines = {
225 self.EXPR: RevisionSearch,
226 self.PATH: PathSearch,
227 self.MESSAGE: MessageSearch,
228 self.DIFF: DiffSearch,
229 self.AUTHOR: AuthorSearch,
230 self.COMMITTER: CommitterSearch,
231 self.DATE_RANGE: DateRangeSearch,
234 self.modes = (self.EXPR, self.PATH, self.DATE_RANGE,
235 self.DIFF, self.MESSAGE, self.AUTHOR, self.COMMITTER)
236 self.mode_combo.addItems(self.modes)
238 connect_button(self.search_button, self.search_callback)
239 connect_button(self.browse_button, self.browse_callback)
240 connect_button(self.button_export, self.export_patch)
241 connect_button(self.button_cherrypick, self.cherry_pick)
242 connect_button(self.button_close, self.accept)
244 self.connect(self.mode_combo, SIGNAL('currentIndexChanged(int)'),
245 self.mode_index_changed)
247 self.connect(self.commit_list,
248 SIGNAL('itemSelectionChanged()'),
249 self.display)
251 self.set_start_date(mkdate(time.time()-(87640*31)))
252 self.set_end_date(mkdate(time.time()+87640))
253 self.set_mode(self.EXPR)
255 self.query.setFocus()
257 def mode_index_changed(self, idx):
258 mode = self.mode()
259 self.update_shown_widgets(mode)
260 if mode == self.PATH:
261 self.browse_callback()
263 def set_commits(self, commits):
264 widget = self.commit_list
265 widget.clear()
266 widget.addItems(commits)
268 def set_start_date(self, datestr):
269 self.set_date(self.start_date, datestr)
271 def set_end_date(self, datestr):
272 self.set_date(self.end_date, datestr)
274 def set_date(self, widget, datestr):
275 fmt = Qt.ISODate
276 date = QtCore.QDate.fromString(datestr, fmt)
277 if date:
278 widget.setDate(date)
280 def set_mode(self, mode):
281 idx = self.modes.index(mode)
282 self.mode_combo.setCurrentIndex(idx)
283 self.update_shown_widgets(mode)
285 def update_shown_widgets(self, mode):
286 date_shown = mode == self.DATE_RANGE
287 browse_shown = mode == self.PATH
288 self.query.setVisible(not date_shown)
289 self.browse_button.setVisible(browse_shown)
290 self.start_date.setVisible(date_shown)
291 self.end_date.setVisible(date_shown)
293 def mode(self):
294 return self.mode_combo.currentText()
296 def search_callback(self, *args):
297 engineclass = self.engines[self.mode()]
298 self.model.query = self.query.text()
299 self.model.max_count = self.max_count.value()
301 fmt = Qt.ISODate
302 self.model.start_date = self.start_date.date().toString(fmt)
303 self.model.end_date = self.end_date.date().toString(fmt)
305 self.results = engineclass(self.model).search()
306 if self.results:
307 self.display_results()
308 else:
309 self.commit_list.clear()
310 self.commit_text.setText('')
312 def browse_callback(self):
313 paths = QtGui.QFileDialog.getOpenFileNames(self,
314 N_('Choose Path(s)'))
315 if not paths:
316 return
317 filepaths = []
318 curdir = core.getcwd()
319 prefix_len = len(curdir) + 1
320 for path in paths:
321 if not path.startswith(curdir):
322 continue
323 relpath = path[prefix_len:]
324 if relpath:
325 filepaths.append(relpath)
327 query = core.list2cmdline(filepaths)
328 self.query.setText(query)
329 if query:
330 self.search_callback()
332 def display_results(self):
333 commits = [result[1] for result in self.results]
334 self.set_commits(commits)
336 def selected_revision(self):
337 result = qtutils.selected_item(self.commit_list, self.results)
338 if result is None:
339 return None
340 else:
341 return result[0]
343 def display(self, *args):
344 revision = self.selected_revision()
345 if revision is None:
346 self.commit_text.setText('')
347 else:
348 qtutils.set_clipboard(revision)
349 diff = gitcmds.commit_diff(revision)
350 self.commit_text.setText(diff)
352 def export_patch(self):
353 revision = self.selected_revision()
354 if revision is not None:
355 Interaction.log_status(*gitcmds.export_patchset(revision,
356 revision))
358 def cherry_pick(self):
359 revision = self.selected_revision()
360 if revision is not None:
361 Interaction.log_status(*git.cherry_pick(revision))
364 def search_commits(parent):
365 opts = SearchOptions()
366 widget = Search(opts, parent)
367 widget.show()
368 return widget
371 if __name__ == '__main__':
372 import sys
373 app = QtGui.QApplication(sys.argv)
374 widget = Search()
375 widget.show()
376 sys.exit(app.exec_())