core: add list2cmdline() wrapper
[git-cola.git] / cola / widgets / search.py
blob9794fd50b3dff0e5bb05cd2ee1c734a595078d05
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
25 from cola.compat import ustr
28 def mkdate(timespec):
29 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
32 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):
41 def __init__(self, parent):
42 standard.Dialog.__init__(self, parent)
43 self.setAttribute(Qt.WA_MacMetalStyle)
44 self.setWindowTitle(N_('Search'))
46 self.mode_combo = QtGui.QComboBox()
47 self.browse_button = create_toolbutton(icon=icons.folder(),
48 tooltip=N_('Browse...'))
49 self.query = QtGui.QLineEdit()
51 self.start_date = QtGui.QDateEdit()
52 self.start_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
53 self.start_date.setCalendarPopup(True)
54 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
56 self.end_date = QtGui.QDateEdit()
57 self.end_date.setCurrentSection(QtGui.QDateTimeEdit.YearSection)
58 self.end_date.setCalendarPopup(True)
59 self.end_date.setDisplayFormat(N_('yyyy-MM-dd'))
61 icon = icons.search()
62 self.search_button = qtutils.create_button(text=N_('Search'),
63 icon=icon, default=True)
65 self.max_count = QtGui.QSpinBox()
66 self.max_count.setMinimum(5)
67 self.max_count.setMaximum(9995)
68 self.max_count.setSingleStep(5)
69 self.max_count.setValue(500)
71 self.commit_list = QtGui.QListWidget()
72 self.commit_list.setMinimumSize(QtCore.QSize(1, 1))
73 self.commit_list.setAlternatingRowColors(True)
74 selection_mode = QtGui.QAbstractItemView.SingleSelection
75 self.commit_list.setSelectionMode(selection_mode)
77 self.commit_text = DiffTextEdit(self, whitespace=False)
79 self.button_export = qtutils.create_button(text=N_('Export Patches'),
80 icon=icons.diff())
82 self.button_cherrypick = qtutils.create_button(text=N_('Cherry Pick'),
83 icon=icons.save())
84 self.button_close = qtutils.close_button()
86 self.top_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
87 self.query, self.start_date,
88 self.end_date, self.browse_button,
89 self.search_button, qtutils.STRETCH,
90 self.mode_combo, self.max_count)
92 self.splitter = qtutils.splitter(Qt.Vertical,
93 self.commit_list, self.commit_text)
95 self.bottom_layout = qtutils.hbox(defs.no_margin, defs.spacing,
96 self.button_export,
97 self.button_cherrypick,
98 qtutils.STRETCH, self.button_close)
100 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
101 self.top_layout, self.splitter,
102 self.bottom_layout)
103 self.setLayout(self.main_layout)
105 if self.parent():
106 self.resize(self.parent().width(), self.parent().height())
107 else:
108 self.resize(720, 500)
111 def search():
112 """Return a callback to handle various search actions."""
113 return search_commits(qtutils.active_window())
116 class SearchEngine(object):
117 def __init__(self, model):
118 self.model = model
120 def rev_args(self):
121 max_count = self.model.max_count
122 return {
123 'no_color': True,
124 'max-count': max_count,
125 'pretty': 'format:%H %aN - %s - %ar',
128 def common_args(self):
129 return (self.model.query, self.rev_args())
131 def search(self):
132 if not self.validate():
133 return
134 return self.results()
136 def validate(self):
137 return len(self.model.query) > 1
139 def revisions(self, *args, **kwargs):
140 revlist = git.log(*args, **kwargs)[STDOUT]
141 return gitcmds.parse_rev_list(revlist)
143 def results(self):
144 pass
146 class RevisionSearch(SearchEngine):
147 def results(self):
148 query, opts = self.common_args()
149 args = utils.shell_split(query)
150 return self.revisions(all=True, *args, **opts)
153 class PathSearch(SearchEngine):
154 def results(self):
155 query, args = self.common_args()
156 paths = ['--'] + utils.shell_split(query)
157 return self.revisions(all=True, *paths, **args)
160 class MessageSearch(SearchEngine):
161 def results(self):
162 query, kwargs = self.common_args()
163 return self.revisions(all=True, grep=query, **kwargs)
166 class AuthorSearch(SearchEngine):
167 def results(self):
168 query, kwargs = self.common_args()
169 return self.revisions(all=True, author=query, **kwargs)
172 class CommitterSearch(SearchEngine):
173 def results(self):
174 query, kwargs = self.common_args()
175 return self.revisions(all=True, committer=query, **kwargs)
178 class DiffSearch(SearchEngine):
179 def results(self):
180 query, kwargs = self.common_args()
181 return gitcmds.parse_rev_list(
182 git.log('-S'+query, all=True, **kwargs)[STDOUT])
185 class DateRangeSearch(SearchEngine):
186 def validate(self):
187 return self.model.start_date < self.model.end_date
189 def results(self):
190 kwargs = self.rev_args()
191 start_date = self.model.start_date
192 end_date = self.model.end_date
193 return self.revisions(date='iso',
194 all=True,
195 after=start_date,
196 before=end_date,
197 **kwargs)
200 class Search(SearchWidget):
202 def __init__(self, model, parent):
203 SearchWidget.__init__(self, parent)
204 self.model = model
206 self.EXPR = N_('Search by Expression')
207 self.PATH = N_('Search by Path')
208 self.MESSAGE = N_('Search Commit Messages')
209 self.DIFF = N_('Search Diffs')
210 self.AUTHOR = N_('Search Authors')
211 self.COMMITTER = N_('Search Committers')
212 self.DATE_RANGE = N_('Search Date Range')
214 # Each search type is handled by a distinct SearchEngine subclass
215 self.engines = {
216 self.EXPR: RevisionSearch,
217 self.PATH: PathSearch,
218 self.MESSAGE: MessageSearch,
219 self.DIFF: DiffSearch,
220 self.AUTHOR: AuthorSearch,
221 self.COMMITTER: CommitterSearch,
222 self.DATE_RANGE: DateRangeSearch,
225 self.modes = (self.EXPR, self.PATH, self.DATE_RANGE,
226 self.DIFF, self.MESSAGE, self.AUTHOR, self.COMMITTER)
227 self.mode_combo.addItems(self.modes)
229 connect_button(self.search_button, self.search_callback)
230 connect_button(self.browse_button, self.browse_callback)
231 connect_button(self.button_export, self.export_patch)
232 connect_button(self.button_cherrypick, self.cherry_pick)
233 connect_button(self.button_close, self.accept)
235 self.connect(self.mode_combo, SIGNAL('currentIndexChanged(int)'),
236 self.mode_index_changed)
238 self.connect(self.commit_list,
239 SIGNAL('itemSelectionChanged()'),
240 self.display)
242 self.set_start_date(mkdate(time.time()-(87640*31)))
243 self.set_end_date(mkdate(time.time()+87640))
244 self.set_mode(self.EXPR)
246 self.query.setFocus()
248 def mode_index_changed(self, idx):
249 mode = self.mode()
250 self.update_shown_widgets(mode)
251 if mode == self.PATH:
252 self.browse_callback()
254 def set_commit_list(self, commits):
255 widget = self.commit_list
256 widget.clear()
257 widget.addItems(commits)
259 def set_start_date(self, datestr):
260 self.set_date(self.start_date, datestr)
262 def set_end_date(self, datestr):
263 self.set_date(self.end_date, datestr)
265 def set_date(self, widget, datestr):
266 fmt = Qt.ISODate
267 date = QtCore.QDate.fromString(datestr, fmt)
268 if date:
269 widget.setDate(date)
271 def set_mode(self, mode):
272 idx = self.modes.index(mode)
273 self.mode_combo.setCurrentIndex(idx)
274 self.update_shown_widgets(mode)
276 def update_shown_widgets(self, mode):
277 date_shown = mode == self.DATE_RANGE
278 browse_shown = mode == self.PATH
279 self.query.setVisible(not date_shown)
280 self.browse_button.setVisible(browse_shown)
281 self.start_date.setVisible(date_shown)
282 self.end_date.setVisible(date_shown)
284 def mode(self):
285 return ustr(self.mode_combo.currentText())
287 def search_callback(self, *args):
288 engineclass = self.engines[self.mode()]
289 self.model.query = ustr(self.query.text())
290 self.model.max_count = self.max_count.value()
292 fmt = Qt.ISODate
293 self.model.start_date = str(self.start_date.date().toString(fmt))
294 self.model.end_date = str(self.end_date.date().toString(fmt))
296 self.results = engineclass(self.model).search()
297 if self.results:
298 self.display_results()
299 else:
300 self.commit_list.clear()
301 self.commit_text.setText('')
303 def browse_callback(self):
304 paths = QtGui.QFileDialog.getOpenFileNames(self,
305 N_('Choose Path(s)'))
306 if not paths:
307 return
308 filepaths = []
309 lenprefix = len(core.getcwd()) + 1
310 for path in map(lambda x: ustr(x), paths):
311 if not path.startswith(core.getcwd()):
312 continue
313 filepaths.append(path[lenprefix:])
314 query = core.list2cmdline(filepaths)
315 self.query.setText(query)
316 if query:
317 self.search_callback()
319 def display_results(self):
320 commit_list = [result[1] for result in self.results]
321 self.set_commit_list(commit_list)
323 def selected_revision(self):
324 result = qtutils.selected_item(self.commit_list, self.results)
325 if result is None:
326 return None
327 else:
328 return result[0]
330 def display(self, *args):
331 revision = self.selected_revision()
332 if revision is None:
333 self.commit_text.setText('')
334 else:
335 qtutils.set_clipboard(revision)
336 diff = gitcmds.commit_diff(revision)
337 self.commit_text.setText(diff)
339 def export_patch(self):
340 revision = self.selected_revision()
341 if revision is not None:
342 Interaction.log_status(*gitcmds.export_patchset(revision,
343 revision))
345 def cherry_pick(self):
346 revision = self.selected_revision()
347 if revision is not None:
348 Interaction.log_status(*git.cherry_pick(revision))
351 def search_commits(parent):
352 opts = SearchOptions()
353 widget = Search(opts, parent)
354 widget.show()
355 return widget
359 if __name__ == '__main__':
360 import sys
361 app = QtGui.QApplication(sys.argv)
362 search = Search()
363 search.show()
364 sys.exit(app.exec_())