doc: add Thomas to the credits
[git-cola.git] / cola / widgets / search.py
blob87213b927bb328af59226715590ab088823a8a0a
1 """A widget for searching git commits"""
2 from __future__ import division, absolute_import, unicode_literals
3 import time
5 from qtpy import QtCore
6 from qtpy import QtWidgets
7 from qtpy.QtCore import Qt
9 from ..i18n import N_
10 from ..interaction import Interaction
11 from ..git import STDOUT
12 from ..qtutils import connect_button
13 from ..qtutils import create_toolbutton
14 from ..qtutils import get
15 from .. import core
16 from .. import gitcmds
17 from .. import icons
18 from .. import utils
19 from .. import qtutils
20 from . import diff
21 from . import defs
22 from . import standard
25 def mkdate(timespec):
26 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
29 class SearchOptions(object):
31 def __init__(self):
32 self.query = ''
33 self.max_count = 500
34 self.start_date = ''
35 self.end_date = ''
38 class SearchWidget(standard.Dialog):
40 def __init__(self, context, parent):
41 standard.Dialog.__init__(self, parent)
43 self.context = context
44 self.setWindowTitle(N_('Search'))
46 self.mode_combo = QtWidgets.QComboBox()
47 self.browse_button = create_toolbutton(icon=icons.folder(),
48 tooltip=N_('Browse...'))
49 self.query = QtWidgets.QLineEdit()
51 self.start_date = QtWidgets.QDateEdit()
52 self.start_date.setCurrentSection(QtWidgets.QDateTimeEdit.YearSection)
53 self.start_date.setCalendarPopup(True)
54 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
56 self.end_date = QtWidgets.QDateEdit()
57 self.end_date.setCurrentSection(QtWidgets.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)
64 self.max_count = standard.SpinBox(value=500, mini=5, maxi=9995, step=5)
66 self.commit_list = QtWidgets.QListWidget()
67 self.commit_list.setMinimumSize(QtCore.QSize(1, 1))
68 self.commit_list.setAlternatingRowColors(True)
69 selection_mode = QtWidgets.QAbstractItemView.SingleSelection
70 self.commit_list.setSelectionMode(selection_mode)
72 self.commit_text = diff.DiffTextEdit(context, self, whitespace=False)
74 self.button_export = qtutils.create_button(text=N_('Export Patches'),
75 icon=icons.diff())
77 self.button_cherrypick = qtutils.create_button(text=N_('Cherry Pick'),
78 icon=icons.save())
79 self.button_close = qtutils.close_button()
81 self.top_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
82 self.query, self.start_date,
83 self.end_date, self.browse_button,
84 self.search_button, qtutils.STRETCH,
85 self.mode_combo, self.max_count)
87 self.splitter = qtutils.splitter(Qt.Vertical,
88 self.commit_list, self.commit_text)
90 self.bottom_layout = qtutils.hbox(defs.no_margin, defs.spacing,
91 self.button_close,
92 qtutils.STRETCH,
93 self.button_export,
94 self.button_cherrypick)
96 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
97 self.top_layout, self.splitter,
98 self.bottom_layout)
99 self.setLayout(self.main_layout)
101 self.init_size(parent=parent)
104 def search(context):
105 """Return a callback to handle various search actions."""
106 return search_commits(context, qtutils.active_window())
109 class SearchEngine(object):
110 def __init__(self, context, model):
111 self.context = context
112 self.model = model
114 def rev_args(self):
115 max_count = self.model.max_count
116 return {
117 'no_color': True,
118 'max-count': max_count,
119 'pretty': 'format:%H %aN - %s - %ar',
122 def common_args(self):
123 return (self.model.query, self.rev_args())
125 def search(self):
126 if self.validate():
127 return self.results()
128 return []
130 def validate(self):
131 return len(self.model.query) > 1
133 def revisions(self, *args, **kwargs):
134 git = self.context.git
135 revlist = git.log(*args, **kwargs)[STDOUT]
136 return gitcmds.parse_rev_list(revlist)
138 def results(self):
139 pass
142 class RevisionSearch(SearchEngine):
144 def results(self):
145 query, opts = self.common_args()
146 args = utils.shell_split(query)
147 return self.revisions(*args, **opts)
150 class PathSearch(SearchEngine):
152 def results(self):
153 query, args = self.common_args()
154 paths = ['--'] + utils.shell_split(query)
155 return self.revisions(all=True, *paths, **args)
158 class MessageSearch(SearchEngine):
160 def results(self):
161 query, kwargs = self.common_args()
162 return self.revisions(all=True, grep=query, **kwargs)
165 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):
174 def results(self):
175 query, kwargs = self.common_args()
176 return self.revisions(all=True, committer=query, **kwargs)
179 class DiffSearch(SearchEngine):
181 def results(self):
182 git = self.context.git
183 query, kwargs = self.common_args()
184 return gitcmds.parse_rev_list(
185 git.log('-S'+query, all=True, **kwargs)[STDOUT])
188 class DateRangeSearch(SearchEngine):
190 def validate(self):
191 return self.model.start_date < self.model.end_date
193 def results(self):
194 kwargs = self.rev_args()
195 start_date = self.model.start_date
196 end_date = self.model.end_date
197 return self.revisions(date='iso',
198 all=True,
199 after=start_date,
200 before=end_date,
201 **kwargs)
204 class Search(SearchWidget):
206 def __init__(self, context, model, parent):
208 Search diffs and commit logs
210 :param model: SearchOptions instance
213 SearchWidget.__init__(self, context, parent)
214 self.model = model
216 self.EXPR = N_('Search by Expression')
217 self.PATH = N_('Search by Path')
218 self.MESSAGE = N_('Search Commit Messages')
219 self.DIFF = N_('Search Diffs')
220 self.AUTHOR = N_('Search Authors')
221 self.COMMITTER = N_('Search Committers')
222 self.DATE_RANGE = N_('Search Date Range')
223 self.results = []
225 # Each search type is handled by a distinct SearchEngine subclass
226 self.engines = {
227 self.EXPR: RevisionSearch,
228 self.PATH: PathSearch,
229 self.MESSAGE: MessageSearch,
230 self.DIFF: DiffSearch,
231 self.AUTHOR: AuthorSearch,
232 self.COMMITTER: CommitterSearch,
233 self.DATE_RANGE: DateRangeSearch,
236 self.modes = (self.EXPR, self.PATH, self.DATE_RANGE,
237 self.DIFF, self.MESSAGE, self.AUTHOR, self.COMMITTER)
238 self.mode_combo.addItems(self.modes)
240 connect_button(self.search_button, self.search_callback)
241 connect_button(self.browse_button, self.browse_callback)
242 connect_button(self.button_export, self.export_patch)
243 connect_button(self.button_cherrypick, self.cherry_pick)
244 connect_button(self.button_close, self.accept)
246 self.mode_combo.currentIndexChanged.connect(self.mode_changed)
247 self.commit_list.itemSelectionChanged.connect(self.display)
249 self.set_start_date(mkdate(time.time()-(87640*31)))
250 self.set_end_date(mkdate(time.time()+87640))
251 self.set_mode(self.EXPR)
253 self.query.setFocus()
255 def mode_changed(self, _idx):
256 mode = self.mode()
257 self.update_shown_widgets(mode)
258 if mode == self.PATH:
259 self.browse_callback()
261 def set_commits(self, commits):
262 widget = self.commit_list
263 widget.clear()
264 widget.addItems(commits)
266 def set_start_date(self, datestr):
267 self.set_date(self.start_date, datestr)
269 def set_end_date(self, datestr):
270 self.set_date(self.end_date, datestr)
272 def set_date(self, widget, datestr):
273 fmt = Qt.ISODate
274 date = QtCore.QDate.fromString(datestr, fmt)
275 if date:
276 widget.setDate(date)
278 def set_mode(self, mode):
279 idx = self.modes.index(mode)
280 self.mode_combo.setCurrentIndex(idx)
281 self.update_shown_widgets(mode)
283 def update_shown_widgets(self, mode):
284 date_shown = mode == self.DATE_RANGE
285 browse_shown = mode == self.PATH
286 self.query.setVisible(not date_shown)
287 self.browse_button.setVisible(browse_shown)
288 self.start_date.setVisible(date_shown)
289 self.end_date.setVisible(date_shown)
291 def mode(self):
292 return self.mode_combo.currentText()
294 # pylint: disable=unused-argument
295 def search_callback(self, *args):
296 engineclass = self.engines[self.mode()]
297 self.model.query = get(self.query)
298 self.model.max_count = get(self.max_count)
300 self.model.start_date = get(self.start_date)
301 self.model.end_date = get(self.end_date)
303 self.results = engineclass(self.context, self.model).search()
304 if self.results:
305 self.display_results()
306 else:
307 self.commit_list.clear()
308 self.commit_text.setText('')
310 def browse_callback(self):
311 paths = qtutils.open_files(N_('Choose Paths'))
312 if not paths:
313 return
314 filepaths = []
315 curdir = core.getcwd()
316 prefix_len = len(curdir) + 1
317 for path in paths:
318 if not path.startswith(curdir):
319 continue
320 relpath = path[prefix_len:]
321 if relpath:
322 filepaths.append(relpath)
324 query = core.list2cmdline(filepaths)
325 self.query.setText(query)
326 if query:
327 self.search_callback()
329 def display_results(self):
330 commits = [result[1] for result in self.results]
331 self.set_commits(commits)
333 def selected_revision(self):
334 result = qtutils.selected_item(self.commit_list, self.results)
335 return result[0] if result else None
337 # pylint: disable=unused-argument
338 def display(self, *args):
339 context = self.context
340 revision = self.selected_revision()
341 if revision is None:
342 self.commit_text.setText('')
343 else:
344 qtutils.set_clipboard(revision)
345 diff_text = gitcmds.commit_diff(context, revision)
346 self.commit_text.setText(diff_text)
348 def export_patch(self):
349 context = self.context
350 revision = self.selected_revision()
351 if revision is not None:
352 Interaction.log_status(*gitcmds.export_patchset(
353 context, revision, revision))
355 def cherry_pick(self):
356 git = self.context.git
357 revision = self.selected_revision()
358 if revision is not None:
359 Interaction.log_status(*git.cherry_pick(revision))
362 def search_commits(context, parent):
363 opts = SearchOptions()
364 widget = Search(context, opts, parent)
365 widget.show()
366 return widget