tree-wide: remove pylint cruft
[git-cola.git] / cola / widgets / search.py
blob2de7bbd81059ac6f141706922bdb14a560e55af9
1 """A widget for searching git commits"""
2 import time
4 from qtpy import QtCore
5 from qtpy import QtWidgets
6 from qtpy.QtCore import Qt
8 from ..i18n import N_
9 from ..interaction import Interaction
10 from ..git import STDOUT
11 from ..qtutils import connect_button
12 from ..qtutils import create_toolbutton
13 from ..qtutils import get
14 from .. import core
15 from .. import gitcmds
16 from .. import icons
17 from .. import utils
18 from .. import qtutils
19 from . import diff
20 from . import defs
21 from . import standard
24 def mkdate(timespec):
25 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
28 class SearchOptions:
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, context, parent):
38 standard.Dialog.__init__(self, parent)
40 self.context = context
41 self.setWindowTitle(N_('Search'))
43 self.mode_combo = QtWidgets.QComboBox()
44 self.browse_button = create_toolbutton(
45 icon=icons.folder(), tooltip=N_('Browse...')
47 self.query = QtWidgets.QLineEdit()
49 self.start_date = QtWidgets.QDateEdit()
50 self.start_date.setCurrentSection(QtWidgets.QDateTimeEdit.YearSection)
51 self.start_date.setCalendarPopup(True)
52 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
54 self.end_date = QtWidgets.QDateEdit()
55 self.end_date.setCurrentSection(QtWidgets.QDateTimeEdit.YearSection)
56 self.end_date.setCalendarPopup(True)
57 self.end_date.setDisplayFormat(N_('yyyy-MM-dd'))
59 icon = icons.search()
60 self.search_button = qtutils.create_button(
61 text=N_('Search'), icon=icon, default=True
63 self.max_count = standard.SpinBox(value=500, mini=5, maxi=9995, step=5)
65 self.commit_list = QtWidgets.QListWidget()
66 self.commit_list.setMinimumSize(QtCore.QSize(10, 10))
67 self.commit_list.setAlternatingRowColors(True)
68 selection_mode = QtWidgets.QAbstractItemView.SingleSelection
69 self.commit_list.setSelectionMode(selection_mode)
71 self.commit_text = diff.DiffTextEdit(context, self, whitespace=False)
73 self.button_export = qtutils.create_button(
74 text=N_('Export Patches'), icon=icons.diff()
77 self.button_cherrypick = qtutils.create_button(
78 text=N_('Cherry Pick'), icon=icons.cherry_pick()
80 self.button_close = qtutils.close_button()
82 self.top_layout = qtutils.hbox(
83 defs.no_margin,
84 defs.button_spacing,
85 self.query,
86 self.start_date,
87 self.end_date,
88 self.browse_button,
89 self.search_button,
90 qtutils.STRETCH,
91 self.mode_combo,
92 self.max_count,
95 self.splitter = qtutils.splitter(
96 Qt.Vertical, self.commit_list, self.commit_text
99 self.bottom_layout = qtutils.hbox(
100 defs.no_margin,
101 defs.spacing,
102 qtutils.STRETCH,
103 self.button_close,
104 self.button_export,
105 self.button_cherrypick,
108 self.main_layout = qtutils.vbox(
109 defs.margin,
110 defs.spacing,
111 self.top_layout,
112 self.splitter,
113 self.bottom_layout,
115 self.setLayout(self.main_layout)
117 self.init_size(parent=parent)
120 def search(context):
121 """Return a callback to handle various search actions."""
122 return search_commits(context, qtutils.active_window())
125 class SearchEngine:
126 def __init__(self, context, model):
127 self.context = context
128 self.model = model
130 def rev_args(self):
131 max_count = self.model.max_count
132 return {
133 'no_color': True,
134 'max-count': max_count,
135 'pretty': 'format:%H %aN - %s - %ar',
138 def common_args(self):
139 return (self.model.query, self.rev_args())
141 def search(self):
142 if self.validate():
143 return self.results()
144 return []
146 def validate(self):
147 return len(self.model.query) > 1
149 def revisions(self, *args, **kwargs):
150 git = self.context.git
151 revlist = git.log(*args, **kwargs)[STDOUT]
152 return gitcmds.parse_rev_list(revlist)
154 def results(self):
155 pass
158 class RevisionSearch(SearchEngine):
159 def results(self):
160 query, opts = self.common_args()
161 args = utils.shell_split(query)
162 return self.revisions(*args, **opts)
165 class PathSearch(SearchEngine):
166 def results(self):
167 query, args = self.common_args()
168 paths = ['--'] + utils.shell_split(query)
169 return self.revisions(all=True, *paths, **args)
172 class MessageSearch(SearchEngine):
173 def results(self):
174 query, kwargs = self.common_args()
175 return self.revisions(all=True, grep=query, **kwargs)
178 class AuthorSearch(SearchEngine):
179 def results(self):
180 query, kwargs = self.common_args()
181 return self.revisions(all=True, author=query, **kwargs)
184 class CommitterSearch(SearchEngine):
185 def results(self):
186 query, kwargs = self.common_args()
187 return self.revisions(all=True, committer=query, **kwargs)
190 class DiffSearch(SearchEngine):
191 def results(self):
192 git = self.context.git
193 query, kwargs = self.common_args()
194 return gitcmds.parse_rev_list(git.log('-S' + query, all=True, **kwargs)[STDOUT])
197 class DateRangeSearch(SearchEngine):
198 def validate(self):
199 return self.model.start_date < self.model.end_date
201 def results(self):
202 kwargs = self.rev_args()
203 start_date = self.model.start_date
204 end_date = self.model.end_date
205 return self.revisions(
206 date='iso', all=True, after=start_date, before=end_date, **kwargs
210 class Search(SearchWidget):
211 def __init__(self, context, model, parent):
213 Search diffs and commit logs
215 :param model: SearchOptions instance
218 SearchWidget.__init__(self, context, parent)
219 self.model = model
221 self.EXPR = N_('Search by Expression')
222 self.PATH = N_('Search by Path')
223 self.MESSAGE = N_('Search Commit Messages')
224 self.DIFF = N_('Search Diffs')
225 self.AUTHOR = N_('Search Authors')
226 self.COMMITTER = N_('Search Committers')
227 self.DATE_RANGE = N_('Search Date Range')
228 self.results = []
230 # Each search type is handled by a distinct SearchEngine subclass
231 self.engines = {
232 self.EXPR: RevisionSearch,
233 self.PATH: PathSearch,
234 self.MESSAGE: MessageSearch,
235 self.DIFF: DiffSearch,
236 self.AUTHOR: AuthorSearch,
237 self.COMMITTER: CommitterSearch,
238 self.DATE_RANGE: DateRangeSearch,
241 self.modes = (
242 self.EXPR,
243 self.PATH,
244 self.DATE_RANGE,
245 self.DIFF,
246 self.MESSAGE,
247 self.AUTHOR,
248 self.COMMITTER,
250 self.mode_combo.addItems(self.modes)
252 connect_button(self.search_button, self.search_callback)
253 connect_button(self.browse_button, self.browse_callback)
254 connect_button(self.button_export, self.export_patch)
255 connect_button(self.button_cherrypick, self.cherry_pick)
256 connect_button(self.button_close, self.accept)
258 self.mode_combo.currentIndexChanged.connect(self.mode_changed)
259 self.commit_list.itemSelectionChanged.connect(self.display)
261 self.set_start_date(mkdate(time.time() - (87640 * 31)))
262 self.set_end_date(mkdate(time.time() + 87640))
263 self.set_mode(self.EXPR)
265 self.query.setFocus()
267 def mode_changed(self, _idx):
268 mode = self.mode()
269 self.update_shown_widgets(mode)
270 if mode == self.PATH:
271 self.browse_callback()
273 def set_commits(self, commits):
274 widget = self.commit_list
275 widget.clear()
276 widget.addItems(commits)
278 def set_start_date(self, datestr):
279 set_date(self.start_date, datestr)
281 def set_end_date(self, datestr):
282 set_date(self.end_date, datestr)
284 def set_mode(self, mode):
285 idx = self.modes.index(mode)
286 self.mode_combo.setCurrentIndex(idx)
287 self.update_shown_widgets(mode)
289 def update_shown_widgets(self, mode):
290 date_shown = mode == self.DATE_RANGE
291 browse_shown = mode == self.PATH
292 self.query.setVisible(not date_shown)
293 self.browse_button.setVisible(browse_shown)
294 self.start_date.setVisible(date_shown)
295 self.end_date.setVisible(date_shown)
297 def mode(self):
298 return self.mode_combo.currentText()
300 def search_callback(self, *args):
301 engineclass = self.engines[self.mode()]
302 self.model.query = get(self.query)
303 self.model.max_count = get(self.max_count)
305 self.model.start_date = get(self.start_date)
306 self.model.end_date = get(self.end_date)
308 self.results = engineclass(self.context, self.model).search()
309 if self.results:
310 self.display_results()
311 else:
312 self.commit_list.clear()
313 self.commit_text.setText('')
315 def browse_callback(self):
316 paths = qtutils.open_files(N_('Choose Paths'))
317 if not paths:
318 return
319 filepaths = []
320 curdir = core.getcwd()
321 prefix_len = len(curdir) + 1
322 for path in paths:
323 if not path.startswith(curdir):
324 continue
325 relpath = path[prefix_len:]
326 if relpath:
327 filepaths.append(relpath)
329 query = core.list2cmdline(filepaths)
330 self.query.setText(query)
331 if query:
332 self.search_callback()
334 def display_results(self):
335 commits = [result[1] for result in self.results]
336 self.set_commits(commits)
338 def selected_revision(self):
339 result = qtutils.selected_item(self.commit_list, self.results)
340 return result[0] if result else None
342 def display(self, *args):
343 context = self.context
344 revision = self.selected_revision()
345 if revision is None:
346 self.commit_text.setText('')
347 else:
348 qtutils.set_clipboard(revision)
349 diff_text = gitcmds.commit_diff(context, revision)
350 self.commit_text.setText(diff_text)
352 def export_patch(self):
353 context = self.context
354 revision = self.selected_revision()
355 if revision is not None:
356 Interaction.log_status(
357 *gitcmds.export_patchset(context, revision, revision)
360 def cherry_pick(self):
361 git = self.context.git
362 revision = self.selected_revision()
363 if revision is not None:
364 Interaction.log_status(*git.cherry_pick(revision))
367 def set_date(widget, datestr):
368 fmt = Qt.ISODate
369 date = QtCore.QDate.fromString(datestr, fmt)
370 if date:
371 widget.setDate(date)
374 def search_commits(context, parent):
375 opts = SearchOptions()
376 widget = Search(context, opts, parent)
377 widget.show()
378 return widget