1 """A widget for searching git commits"""
2 from __future__
import division
, absolute_import
, unicode_literals
6 from PyQt4
import QtGui
7 from PyQt4
import QtCore
8 from PyQt4
.QtCore
import Qt
9 from PyQt4
.QtCore
import SIGNAL
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
28 return '%04d-%02d-%02d' % time
.localtime(timespec
)[:3]
31 class SearchOptions(object):
40 class SearchWidget(standard
.Dialog
):
42 def __init__(self
, parent
):
43 standard
.Dialog
.__init
__(self
, parent
)
44 self
.setAttribute(Qt
.WA_MacMetalStyle
)
45 self
.setWindowTitle(N_('Search'))
47 self
.mode_combo
= QtGui
.QComboBox()
48 self
.browse_button
= create_toolbutton(icon
=icons
.folder(),
49 tooltip
=N_('Browse...'))
50 self
.query
= QtGui
.QLineEdit()
52 self
.start_date
= QtGui
.QDateEdit()
53 self
.start_date
.setCurrentSection(QtGui
.QDateTimeEdit
.YearSection
)
54 self
.start_date
.setCalendarPopup(True)
55 self
.start_date
.setDisplayFormat(N_('yyyy-MM-dd'))
57 self
.end_date
= QtGui
.QDateEdit()
58 self
.end_date
.setCurrentSection(QtGui
.QDateTimeEdit
.YearSection
)
59 self
.end_date
.setCalendarPopup(True)
60 self
.end_date
.setDisplayFormat(N_('yyyy-MM-dd'))
63 self
.search_button
= qtutils
.create_button(text
=N_('Search'),
64 icon
=icon
, default
=True)
66 self
.max_count
= QtGui
.QSpinBox()
67 self
.max_count
.setMinimum(5)
68 self
.max_count
.setMaximum(9995)
69 self
.max_count
.setSingleStep(5)
70 self
.max_count
.setValue(500)
72 self
.commit_list
= QtGui
.QListWidget()
73 self
.commit_list
.setMinimumSize(QtCore
.QSize(1, 1))
74 self
.commit_list
.setAlternatingRowColors(True)
75 selection_mode
= QtGui
.QAbstractItemView
.SingleSelection
76 self
.commit_list
.setSelectionMode(selection_mode
)
78 self
.commit_text
= DiffTextEdit(self
, whitespace
=False)
80 self
.button_export
= qtutils
.create_button(text
=N_('Export Patches'),
83 self
.button_cherrypick
= qtutils
.create_button(text
=N_('Cherry Pick'),
85 self
.button_close
= qtutils
.close_button()
87 self
.top_layout
= qtutils
.hbox(defs
.no_margin
, defs
.button_spacing
,
88 self
.query
, self
.start_date
,
89 self
.end_date
, self
.browse_button
,
90 self
.search_button
, qtutils
.STRETCH
,
91 self
.mode_combo
, self
.max_count
)
93 self
.splitter
= qtutils
.splitter(Qt
.Vertical
,
94 self
.commit_list
, self
.commit_text
)
96 self
.bottom_layout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
,
98 self
.button_cherrypick
,
99 qtutils
.STRETCH
, self
.button_close
)
101 self
.main_layout
= qtutils
.vbox(defs
.margin
, defs
.spacing
,
102 self
.top_layout
, self
.splitter
,
104 self
.setLayout(self
.main_layout
)
107 self
.resize(self
.parent().width(), self
.parent().height())
109 self
.resize(720, 500)
113 """Return a callback to handle various search actions."""
114 return search_commits(qtutils
.active_window())
117 class SearchEngine(object):
118 def __init__(self
, model
):
122 max_count
= self
.model
.max_count
125 'max-count': max_count
,
126 'pretty': 'format:%H %aN - %s - %ar',
129 def common_args(self
):
130 return (self
.model
.query
, self
.rev_args())
133 if not self
.validate():
135 return self
.results()
138 return len(self
.model
.query
) > 1
140 def revisions(self
, *args
, **kwargs
):
141 revlist
= git
.log(*args
, **kwargs
)[STDOUT
]
142 return gitcmds
.parse_rev_list(revlist
)
148 class RevisionSearch(SearchEngine
):
151 query
, opts
= self
.common_args()
152 args
= utils
.shell_split(query
)
153 return self
.revisions(all
=True, *args
, **opts
)
156 class PathSearch(SearchEngine
):
159 query
, args
= self
.common_args()
160 paths
= ['--'] + utils
.shell_split(query
)
161 return self
.revisions(all
=True, *paths
, **args
)
164 class MessageSearch(SearchEngine
):
167 query
, kwargs
= self
.common_args()
168 return self
.revisions(all
=True, grep
=query
, **kwargs
)
171 class AuthorSearch(SearchEngine
):
174 query
, kwargs
= self
.common_args()
175 return self
.revisions(all
=True, author
=query
, **kwargs
)
178 class CommitterSearch(SearchEngine
):
181 query
, kwargs
= self
.common_args()
182 return self
.revisions(all
=True, committer
=query
, **kwargs
)
185 class DiffSearch(SearchEngine
):
188 query
, kwargs
= self
.common_args()
189 return gitcmds
.parse_rev_list(
190 git
.log('-S'+query
, all
=True, **kwargs
)[STDOUT
])
193 class DateRangeSearch(SearchEngine
):
196 return self
.model
.start_date
< self
.model
.end_date
199 kwargs
= self
.rev_args()
200 start_date
= self
.model
.start_date
201 end_date
= self
.model
.end_date
202 return self
.revisions(date
='iso',
209 class Search(SearchWidget
):
211 def __init__(self
, model
, parent
):
212 SearchWidget
.__init
__(self
, parent
)
215 self
.EXPR
= N_('Search by Expression')
216 self
.PATH
= N_('Search by Path')
217 self
.MESSAGE
= N_('Search Commit Messages')
218 self
.DIFF
= N_('Search Diffs')
219 self
.AUTHOR
= N_('Search Authors')
220 self
.COMMITTER
= N_('Search Committers')
221 self
.DATE_RANGE
= N_('Search Date Range')
223 # Each search type is handled by a distinct SearchEngine subclass
225 self
.EXPR
: RevisionSearch
,
226 self
.PATH
: PathSearch
,
227 self
.MESSAGE
: MessageSearch
,
228 self
.DIFF
: DiffSearch
,
229 self
.AUTHOR
: AuthorSearch
,
230 self
.COMMITTER
: CommitterSearch
,
231 self
.DATE_RANGE
: DateRangeSearch
,
234 self
.modes
= (self
.EXPR
, self
.PATH
, self
.DATE_RANGE
,
235 self
.DIFF
, self
.MESSAGE
, self
.AUTHOR
, self
.COMMITTER
)
236 self
.mode_combo
.addItems(self
.modes
)
238 connect_button(self
.search_button
, self
.search_callback
)
239 connect_button(self
.browse_button
, self
.browse_callback
)
240 connect_button(self
.button_export
, self
.export_patch
)
241 connect_button(self
.button_cherrypick
, self
.cherry_pick
)
242 connect_button(self
.button_close
, self
.accept
)
244 self
.connect(self
.mode_combo
, SIGNAL('currentIndexChanged(int)'),
245 self
.mode_index_changed
)
247 self
.connect(self
.commit_list
,
248 SIGNAL('itemSelectionChanged()'),
251 self
.set_start_date(mkdate(time
.time()-(87640*31)))
252 self
.set_end_date(mkdate(time
.time()+87640))
253 self
.set_mode(self
.EXPR
)
255 self
.query
.setFocus()
257 def mode_index_changed(self
, idx
):
259 self
.update_shown_widgets(mode
)
260 if mode
== self
.PATH
:
261 self
.browse_callback()
263 def set_commits(self
, commits
):
264 widget
= self
.commit_list
266 widget
.addItems(commits
)
268 def set_start_date(self
, datestr
):
269 self
.set_date(self
.start_date
, datestr
)
271 def set_end_date(self
, datestr
):
272 self
.set_date(self
.end_date
, datestr
)
274 def set_date(self
, widget
, datestr
):
276 date
= QtCore
.QDate
.fromString(datestr
, fmt
)
280 def set_mode(self
, mode
):
281 idx
= self
.modes
.index(mode
)
282 self
.mode_combo
.setCurrentIndex(idx
)
283 self
.update_shown_widgets(mode
)
285 def update_shown_widgets(self
, mode
):
286 date_shown
= mode
== self
.DATE_RANGE
287 browse_shown
= mode
== self
.PATH
288 self
.query
.setVisible(not date_shown
)
289 self
.browse_button
.setVisible(browse_shown
)
290 self
.start_date
.setVisible(date_shown
)
291 self
.end_date
.setVisible(date_shown
)
294 return self
.mode_combo
.currentText()
296 def search_callback(self
, *args
):
297 engineclass
= self
.engines
[self
.mode()]
298 self
.model
.query
= self
.query
.text()
299 self
.model
.max_count
= self
.max_count
.value()
302 self
.model
.start_date
= self
.start_date
.date().toString(fmt
)
303 self
.model
.end_date
= self
.end_date
.date().toString(fmt
)
305 self
.results
= engineclass(self
.model
).search()
307 self
.display_results()
309 self
.commit_list
.clear()
310 self
.commit_text
.setText('')
312 def browse_callback(self
):
313 paths
= QtGui
.QFileDialog
.getOpenFileNames(self
,
314 N_('Choose Path(s)'))
318 curdir
= core
.getcwd()
319 prefix_len
= len(curdir
) + 1
321 if not path
.startswith(curdir
):
323 relpath
= path
[prefix_len
:]
325 filepaths
.append(relpath
)
327 query
= core
.list2cmdline(filepaths
)
328 self
.query
.setText(query
)
330 self
.search_callback()
332 def display_results(self
):
333 commits
= [result
[1] for result
in self
.results
]
334 self
.set_commits(commits
)
336 def selected_revision(self
):
337 result
= qtutils
.selected_item(self
.commit_list
, self
.results
)
343 def display(self
, *args
):
344 revision
= self
.selected_revision()
346 self
.commit_text
.setText('')
348 qtutils
.set_clipboard(revision
)
349 diff
= gitcmds
.commit_diff(revision
)
350 self
.commit_text
.setText(diff
)
352 def export_patch(self
):
353 revision
= self
.selected_revision()
354 if revision
is not None:
355 Interaction
.log_status(*gitcmds
.export_patchset(revision
,
358 def cherry_pick(self
):
359 revision
= self
.selected_revision()
360 if revision
is not None:
361 Interaction
.log_status(*git
.cherry_pick(revision
))
364 def search_commits(parent
):
365 opts
= SearchOptions()
366 widget
= Search(opts
, parent
)
371 if __name__
== '__main__':
373 app
= QtGui
.QApplication(sys
.argv
)
376 sys
.exit(app
.exec_())