diff: move selected_lines() and selected_text() to DiffTextEdit
[git-cola.git] / cola / widgets / search.py
blob6777e37c6a9466caca2d9d3578cddee877efe72e
1 """A widget for searching git commits"""
2 from __future__ import absolute_import, division, print_function, 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):
30 def __init__(self):
31 self.query = ''
32 self.max_count = 500
33 self.start_date = ''
34 self.end_date = ''
37 class SearchWidget(standard.Dialog):
38 def __init__(self, context, parent):
39 standard.Dialog.__init__(self, parent)
41 self.context = context
42 self.setWindowTitle(N_('Search'))
44 self.mode_combo = QtWidgets.QComboBox()
45 self.browse_button = create_toolbutton(
46 icon=icons.folder(), tooltip=N_('Browse...')
48 self.query = QtWidgets.QLineEdit()
50 self.start_date = QtWidgets.QDateEdit()
51 self.start_date.setCurrentSection(QtWidgets.QDateTimeEdit.YearSection)
52 self.start_date.setCalendarPopup(True)
53 self.start_date.setDisplayFormat(N_('yyyy-MM-dd'))
55 self.end_date = QtWidgets.QDateEdit()
56 self.end_date.setCurrentSection(QtWidgets.QDateTimeEdit.YearSection)
57 self.end_date.setCalendarPopup(True)
58 self.end_date.setDisplayFormat(N_('yyyy-MM-dd'))
60 icon = icons.search()
61 self.search_button = qtutils.create_button(
62 text=N_('Search'), 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(10, 10))
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(
75 text=N_('Export Patches'), icon=icons.diff()
78 self.button_cherrypick = qtutils.create_button(
79 text=N_('Cherry Pick'), icon=icons.cherry_pick()
81 self.button_close = qtutils.close_button()
83 self.top_layout = qtutils.hbox(
84 defs.no_margin,
85 defs.button_spacing,
86 self.query,
87 self.start_date,
88 self.end_date,
89 self.browse_button,
90 self.search_button,
91 qtutils.STRETCH,
92 self.mode_combo,
93 self.max_count,
96 self.splitter = qtutils.splitter(
97 Qt.Vertical, self.commit_list, self.commit_text
100 self.bottom_layout = qtutils.hbox(
101 defs.no_margin,
102 defs.spacing,
103 qtutils.STRETCH,
104 self.button_close,
105 self.button_export,
106 self.button_cherrypick,
109 self.main_layout = qtutils.vbox(
110 defs.margin,
111 defs.spacing,
112 self.top_layout,
113 self.splitter,
114 self.bottom_layout,
116 self.setLayout(self.main_layout)
118 self.init_size(parent=parent)
121 def search(context):
122 """Return a callback to handle various search actions."""
123 return search_commits(context, qtutils.active_window())
126 class SearchEngine(object):
127 def __init__(self, context, model):
128 self.context = context
129 self.model = model
131 def rev_args(self):
132 max_count = self.model.max_count
133 return {
134 'no_color': True,
135 'max-count': max_count,
136 'pretty': 'format:%H %aN - %s - %ar',
139 def common_args(self):
140 return (self.model.query, self.rev_args())
142 def search(self):
143 if self.validate():
144 return self.results()
145 return []
147 def validate(self):
148 return len(self.model.query) > 1
150 def revisions(self, *args, **kwargs):
151 git = self.context.git
152 revlist = git.log(*args, **kwargs)[STDOUT]
153 return gitcmds.parse_rev_list(revlist)
155 def results(self):
156 pass
159 class RevisionSearch(SearchEngine):
160 def results(self):
161 query, opts = self.common_args()
162 args = utils.shell_split(query)
163 return self.revisions(*args, **opts)
166 class PathSearch(SearchEngine):
167 def results(self):
168 query, args = self.common_args()
169 paths = ['--'] + utils.shell_split(query)
170 return self.revisions(all=True, *paths, **args)
173 class MessageSearch(SearchEngine):
174 def results(self):
175 query, kwargs = self.common_args()
176 return self.revisions(all=True, grep=query, **kwargs)
179 class AuthorSearch(SearchEngine):
180 def results(self):
181 query, kwargs = self.common_args()
182 return self.revisions(all=True, author=query, **kwargs)
185 class CommitterSearch(SearchEngine):
186 def results(self):
187 query, kwargs = self.common_args()
188 return self.revisions(all=True, committer=query, **kwargs)
191 class DiffSearch(SearchEngine):
192 def results(self):
193 git = self.context.git
194 query, kwargs = self.common_args()
195 return gitcmds.parse_rev_list(git.log('-S' + query, all=True, **kwargs)[STDOUT])
198 class DateRangeSearch(SearchEngine):
199 def validate(self):
200 return self.model.start_date < self.model.end_date
202 def results(self):
203 kwargs = self.rev_args()
204 start_date = self.model.start_date
205 end_date = self.model.end_date
206 return self.revisions(
207 date='iso', all=True, after=start_date, before=end_date, **kwargs
211 class Search(SearchWidget):
212 def __init__(self, context, model, parent):
214 Search diffs and commit logs
216 :param model: SearchOptions instance
219 SearchWidget.__init__(self, context, parent)
220 self.model = model
222 self.EXPR = N_('Search by Expression')
223 self.PATH = N_('Search by Path')
224 self.MESSAGE = N_('Search Commit Messages')
225 self.DIFF = N_('Search Diffs')
226 self.AUTHOR = N_('Search Authors')
227 self.COMMITTER = N_('Search Committers')
228 self.DATE_RANGE = N_('Search Date Range')
229 self.results = []
231 # Each search type is handled by a distinct SearchEngine subclass
232 self.engines = {
233 self.EXPR: RevisionSearch,
234 self.PATH: PathSearch,
235 self.MESSAGE: MessageSearch,
236 self.DIFF: DiffSearch,
237 self.AUTHOR: AuthorSearch,
238 self.COMMITTER: CommitterSearch,
239 self.DATE_RANGE: DateRangeSearch,
242 self.modes = (
243 self.EXPR,
244 self.PATH,
245 self.DATE_RANGE,
246 self.DIFF,
247 self.MESSAGE,
248 self.AUTHOR,
249 self.COMMITTER,
251 self.mode_combo.addItems(self.modes)
253 connect_button(self.search_button, self.search_callback)
254 connect_button(self.browse_button, self.browse_callback)
255 connect_button(self.button_export, self.export_patch)
256 connect_button(self.button_cherrypick, self.cherry_pick)
257 connect_button(self.button_close, self.accept)
259 # pylint: disable=no-member
260 self.mode_combo.currentIndexChanged.connect(self.mode_changed)
261 self.commit_list.itemSelectionChanged.connect(self.display)
263 self.set_start_date(mkdate(time.time() - (87640 * 31)))
264 self.set_end_date(mkdate(time.time() + 87640))
265 self.set_mode(self.EXPR)
267 self.query.setFocus()
269 def mode_changed(self, _idx):
270 mode = self.mode()
271 self.update_shown_widgets(mode)
272 if mode == self.PATH:
273 self.browse_callback()
275 def set_commits(self, commits):
276 widget = self.commit_list
277 widget.clear()
278 widget.addItems(commits)
280 def set_start_date(self, datestr):
281 set_date(self.start_date, datestr)
283 def set_end_date(self, datestr):
284 set_date(self.end_date, datestr)
286 def set_mode(self, mode):
287 idx = self.modes.index(mode)
288 self.mode_combo.setCurrentIndex(idx)
289 self.update_shown_widgets(mode)
291 def update_shown_widgets(self, mode):
292 date_shown = mode == self.DATE_RANGE
293 browse_shown = mode == self.PATH
294 self.query.setVisible(not date_shown)
295 self.browse_button.setVisible(browse_shown)
296 self.start_date.setVisible(date_shown)
297 self.end_date.setVisible(date_shown)
299 def mode(self):
300 return self.mode_combo.currentText()
302 # pylint: disable=unused-argument
303 def search_callback(self, *args):
304 engineclass = self.engines[self.mode()]
305 self.model.query = get(self.query)
306 self.model.max_count = get(self.max_count)
308 self.model.start_date = get(self.start_date)
309 self.model.end_date = get(self.end_date)
311 self.results = engineclass(self.context, self.model).search()
312 if self.results:
313 self.display_results()
314 else:
315 self.commit_list.clear()
316 self.commit_text.setText('')
318 def browse_callback(self):
319 paths = qtutils.open_files(N_('Choose Paths'))
320 if not paths:
321 return
322 filepaths = []
323 curdir = core.getcwd()
324 prefix_len = len(curdir) + 1
325 for path in paths:
326 if not path.startswith(curdir):
327 continue
328 relpath = path[prefix_len:]
329 if relpath:
330 filepaths.append(relpath)
332 query = core.list2cmdline(filepaths)
333 self.query.setText(query)
334 if query:
335 self.search_callback()
337 def display_results(self):
338 commits = [result[1] for result in self.results]
339 self.set_commits(commits)
341 def selected_revision(self):
342 result = qtutils.selected_item(self.commit_list, self.results)
343 return result[0] if result else None
345 # pylint: disable=unused-argument
346 def display(self, *args):
347 context = self.context
348 revision = self.selected_revision()
349 if revision is None:
350 self.commit_text.setText('')
351 else:
352 qtutils.set_clipboard(revision)
353 diff_text = gitcmds.commit_diff(context, revision)
354 self.commit_text.setText(diff_text)
356 def export_patch(self):
357 context = self.context
358 revision = self.selected_revision()
359 if revision is not None:
360 Interaction.log_status(
361 *gitcmds.export_patchset(context, revision, revision)
364 def cherry_pick(self):
365 git = self.context.git
366 revision = self.selected_revision()
367 if revision is not None:
368 Interaction.log_status(*git.cherry_pick(revision))
371 def set_date(widget, datestr):
372 fmt = Qt.ISODate
373 date = QtCore.QDate.fromString(datestr, fmt)
374 if date:
375 widget.setDate(date)
378 def search_commits(context, parent):
379 opts = SearchOptions()
380 widget = Search(context, opts, parent)
381 widget.show()
382 return widget