1 """A widget for searching git commits"""
2 from __future__
import division
, absolute_import
, unicode_literals
5 from qtpy
import QtCore
6 from qtpy
import QtWidgets
7 from qtpy
.QtCore
import Qt
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
16 from .. import gitcmds
19 from .. import qtutils
22 from . import standard
26 return '%04d-%02d-%02d' % time
.localtime(timespec
)[:3]
29 class SearchOptions(object):
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'))
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'),
77 self
.button_cherrypick
= qtutils
.create_button(text
=N_('Cherry Pick'),
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
,
94 self
.button_cherrypick
)
96 self
.main_layout
= qtutils
.vbox(defs
.margin
, defs
.spacing
,
97 self
.top_layout
, self
.splitter
,
99 self
.setLayout(self
.main_layout
)
101 self
.init_size(parent
=parent
)
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
115 max_count
= self
.model
.max_count
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())
127 return self
.results()
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
)
142 class RevisionSearch(SearchEngine
):
145 query
, opts
= self
.common_args()
146 args
= utils
.shell_split(query
)
147 return self
.revisions(*args
, **opts
)
150 class PathSearch(SearchEngine
):
153 query
, args
= self
.common_args()
154 paths
= ['--'] + utils
.shell_split(query
)
155 return self
.revisions(all
=True, *paths
, **args
)
158 class MessageSearch(SearchEngine
):
161 query
, kwargs
= self
.common_args()
162 return self
.revisions(all
=True, grep
=query
, **kwargs
)
165 class AuthorSearch(SearchEngine
):
168 query
, kwargs
= self
.common_args()
169 return self
.revisions(all
=True, author
=query
, **kwargs
)
172 class CommitterSearch(SearchEngine
):
175 query
, kwargs
= self
.common_args()
176 return self
.revisions(all
=True, committer
=query
, **kwargs
)
179 class DiffSearch(SearchEngine
):
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
):
191 return self
.model
.start_date
< self
.model
.end_date
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',
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
)
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')
225 # Each search type is handled by a distinct SearchEngine subclass
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
):
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
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
):
274 date
= QtCore
.QDate
.fromString(datestr
, fmt
)
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
)
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()
305 self
.display_results()
307 self
.commit_list
.clear()
308 self
.commit_text
.setText('')
310 def browse_callback(self
):
311 paths
= qtutils
.open_files(N_('Choose Paths'))
315 curdir
= core
.getcwd()
316 prefix_len
= len(curdir
) + 1
318 if not path
.startswith(curdir
):
320 relpath
= path
[prefix_len
:]
322 filepaths
.append(relpath
)
324 query
= core
.list2cmdline(filepaths
)
325 self
.query
.setText(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()
342 self
.commit_text
.setText('')
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
)