1 """A widget for searching git commits"""
6 from PyQt4
import QtGui
7 from PyQt4
import QtCore
8 from PyQt4
.QtCore
import SIGNAL
10 from cola
import gitcmds
11 from cola
import utils
12 from cola
import qtutils
13 from cola
.i18n
import N_
14 from cola
.interaction
import Interaction
15 from cola
.git
import git
16 from cola
.qt
import create_toolbutton
17 from cola
.qtutils
import connect_button
18 from cola
.qtutils
import dir_icon
19 from cola
.widgets
import defs
20 from cola
.widgets
import standard
21 from cola
.widgets
.diff
import DiffTextEdit
25 return '%04d-%02d-%02d' % time
.localtime(timespec
)[:3]
28 class SearchOptions(object):
36 class SearchWidget(standard
.Dialog
):
37 def __init__(self
, parent
):
38 standard
.Dialog
.__init
__(self
, parent
)
39 self
.setAttribute(QtCore
.Qt
.WA_MacMetalStyle
)
40 self
.setWindowTitle(N_('Search'))
42 self
.mode_combo
= QtGui
.QComboBox()
43 self
.browse_button
= create_toolbutton(icon
=dir_icon(),
44 tooltip
=N_('Browse...'))
45 self
.query
= QtGui
.QLineEdit()
47 self
.start_date
= QtGui
.QDateEdit()
48 self
.start_date
.setCurrentSection(QtGui
.QDateTimeEdit
.YearSection
)
49 self
.start_date
.setCalendarPopup(True)
50 self
.start_date
.setDisplayFormat(N_('yyyy-MM-dd'))
52 self
.end_date
= QtGui
.QDateEdit()
53 self
.end_date
.setCurrentSection(QtGui
.QDateTimeEdit
.YearSection
)
54 self
.end_date
.setCalendarPopup(True)
55 self
.end_date
.setDisplayFormat(N_('yyyy-MM-dd'))
57 self
.search_button
= QtGui
.QPushButton()
58 self
.search_button
.setText(N_('Search'))
59 self
.search_button
.setDefault(True)
61 self
.max_count
= QtGui
.QSpinBox()
62 self
.max_count
.setMinimum(5)
63 self
.max_count
.setMaximum(9995)
64 self
.max_count
.setSingleStep(5)
65 self
.max_count
.setValue(500)
67 self
.commit_list
= QtGui
.QListWidget()
68 self
.commit_list
.setMinimumSize(QtCore
.QSize(1, 1))
69 self
.commit_list
.setAlternatingRowColors(True)
70 self
.commit_list
.setSelectionMode(QtGui
.QAbstractItemView
.SingleSelection
)
72 self
.commit_text
= DiffTextEdit(self
, whitespace
=False)
74 self
.button_export
= QtGui
.QPushButton()
75 self
.button_export
.setText(N_('Export Patches'))
77 self
.button_cherrypick
= QtGui
.QPushButton()
78 self
.button_cherrypick
.setText(N_('Cherry Pick'))
80 self
.button_close
= QtGui
.QPushButton()
81 self
.button_close
.setText(N_('Close'))
83 self
.top_layout
= QtGui
.QHBoxLayout()
84 self
.top_layout
.setMargin(0)
85 self
.top_layout
.setSpacing(defs
.button_spacing
)
87 self
.top_layout
.addWidget(self
.query
)
88 self
.top_layout
.addWidget(self
.start_date
)
89 self
.top_layout
.addWidget(self
.end_date
)
90 self
.top_layout
.addWidget(self
.browse_button
)
91 self
.top_layout
.addWidget(self
.search_button
)
92 self
.top_layout
.addStretch()
93 self
.top_layout
.addWidget(self
.mode_combo
)
94 self
.top_layout
.addWidget(self
.max_count
)
96 self
.splitter
= QtGui
.QSplitter()
97 self
.splitter
.setHandleWidth(defs
.handle_width
)
98 self
.splitter
.setOrientation(QtCore
.Qt
.Vertical
)
99 self
.splitter
.setChildrenCollapsible(True)
100 self
.splitter
.addWidget(self
.commit_list
)
101 self
.splitter
.addWidget(self
.commit_text
)
103 self
.bottom_layout
= QtGui
.QHBoxLayout()
104 self
.bottom_layout
.setMargin(0)
105 self
.bottom_layout
.setSpacing(defs
.spacing
)
106 self
.bottom_layout
.addWidget(self
.button_export
)
107 self
.bottom_layout
.addWidget(self
.button_cherrypick
)
108 self
.bottom_layout
.addStretch()
109 self
.bottom_layout
.addWidget(self
.button_close
)
111 self
.main_layout
= QtGui
.QVBoxLayout()
112 self
.main_layout
.setMargin(defs
.margin
)
113 self
.main_layout
.setSpacing(defs
.spacing
)
114 self
.main_layout
.addLayout(self
.top_layout
)
115 self
.main_layout
.addWidget(self
.splitter
)
116 self
.main_layout
.addLayout(self
.bottom_layout
)
117 self
.setLayout(self
.main_layout
)
120 self
.resize(self
.parent().width(), self
.parent().height())
122 self
.resize(720, 500)
126 """Return a callback to handle various search actions."""
127 return search_commits(qtutils
.active_window())
130 class SearchEngine(object):
131 def __init__(self
, model
):
135 max_count
= self
.model
.max_count
138 'max-count': max_count
,
139 'pretty': 'format:%H %aN - %s - %ar',
142 def common_args(self
):
143 return (self
.model
.query
, self
.rev_args())
146 if not self
.validate():
148 return self
.results()
151 return len(self
.model
.query
) > 1
153 def revisions(self
, *args
, **kwargs
):
154 revlist
= git
.log(*args
, **kwargs
)
155 return gitcmds
.parse_rev_list(revlist
)
160 class RevisionSearch(SearchEngine
):
162 query
, opts
= self
.common_args()
163 args
= utils
.shell_split(query
)
164 return self
.revisions(all
=True, *args
, **opts
)
167 class PathSearch(SearchEngine
):
169 query
, args
= self
.common_args()
170 paths
= ['--'] + utils
.shell_split(query
)
171 return self
.revisions(all
=True, *paths
, **args
)
174 class MessageSearch(SearchEngine
):
176 query
, kwargs
= self
.common_args()
177 return self
.revisions(all
=True, grep
=query
, **kwargs
)
180 class AuthorSearch(SearchEngine
):
182 query
, kwargs
= self
.common_args()
183 return self
.revisions(all
=True, author
=query
, **kwargs
)
186 class CommitterSearch(SearchEngine
):
188 query
, kwargs
= self
.common_args()
189 return self
.revisions(all
=True, committer
=query
, **kwargs
)
192 class DiffSearch(SearchEngine
):
194 query
, kwargs
= self
.common_args()
195 return gitcmds
.parse_rev_list(
196 git
.log('-S'+query
, all
=True, **kwargs
))
199 class DateRangeSearch(SearchEngine
):
201 return self
.model
.start_date
< self
.model
.end_date
204 kwargs
= self
.rev_args()
205 start_date
= self
.model
.start_date
206 end_date
= self
.model
.end_date
207 return self
.revisions(date
='iso',
214 class Search(SearchWidget
):
216 def __init__(self
, model
, parent
):
217 SearchWidget
.__init
__(self
, parent
)
220 self
.EXPR
= N_('Search by Expression')
221 self
.PATH
= N_('Search by Path')
222 self
.MESSAGE
= N_('Search Commit Messages')
223 self
.DIFF
= N_('Search Diffs')
224 self
.AUTHOR
= N_('Search Authors')
225 self
.COMMITTER
= N_('Search Committers')
226 self
.DATE_RANGE
= N_('Search Date Range')
228 # Each search type is handled by a distinct SearchEngine subclass
230 self
.EXPR
: RevisionSearch
,
231 self
.PATH
: PathSearch
,
232 self
.MESSAGE
: MessageSearch
,
233 self
.DIFF
: DiffSearch
,
234 self
.AUTHOR
: AuthorSearch
,
235 self
.COMMITTER
: CommitterSearch
,
236 self
.DATE_RANGE
: DateRangeSearch
,
239 self
.modes
= (self
.EXPR
, self
.PATH
, self
.DATE_RANGE
,
240 self
.DIFF
, self
.MESSAGE
, self
.AUTHOR
, self
.COMMITTER
)
241 self
.mode_combo
.addItems(self
.modes
)
243 connect_button(self
.search_button
, self
.search_callback
)
244 connect_button(self
.browse_button
, self
.browse_callback
)
245 connect_button(self
.button_export
, self
.export_patch
)
246 connect_button(self
.button_cherrypick
, self
.cherry_pick
)
247 connect_button(self
.button_close
, self
.accept
)
249 self
.connect(self
.mode_combo
, SIGNAL('currentIndexChanged(int)'),
250 self
.mode_index_changed
)
252 self
.connect(self
.commit_list
,
253 SIGNAL('itemSelectionChanged()'),
256 self
.set_start_date(mkdate(time
.time()-(87640*31)))
257 self
.set_end_date(mkdate(time
.time()+87640))
258 self
.set_mode(self
.EXPR
)
260 self
.query
.setFocus()
262 def mode_index_changed(self
, idx
):
264 self
.update_shown_widgets(mode
)
265 if mode
== self
.PATH
:
266 self
.browse_callback()
268 def set_commit_list(self
, commits
):
269 widget
= self
.commit_list
271 widget
.addItems(commits
)
273 def set_start_date(self
, datestr
):
274 self
.set_date(self
.start_date
, datestr
)
276 def set_end_date(self
, datestr
):
277 self
.set_date(self
.end_date
, datestr
)
279 def set_date(self
, widget
, datestr
):
280 fmt
= QtCore
.Qt
.ISODate
281 date
= QtCore
.QDate
.fromString(datestr
, fmt
)
285 def set_mode(self
, mode
):
286 idx
= self
.modes
.index(mode
)
287 self
.mode_combo
.setCurrentIndex(idx
)
288 self
.update_shown_widgets(mode
)
290 def update_shown_widgets(self
, mode
):
291 date_shown
= mode
== self
.DATE_RANGE
292 browse_shown
= mode
== self
.PATH
293 self
.query
.setVisible(not date_shown
)
294 self
.browse_button
.setVisible(browse_shown
)
295 self
.start_date
.setVisible(date_shown
)
296 self
.end_date
.setVisible(date_shown
)
299 return str(self
.mode_combo
.currentText())
301 def search_callback(self
, *args
):
302 engineclass
= self
.engines
[self
.mode()]
303 self
.model
.query
= unicode(self
.query
.text())
304 self
.model
.max_count
= self
.max_count
.value()
306 fmt
= QtCore
.Qt
.ISODate
307 self
.model
.start_date
= str(self
.start_date
.date().toString(fmt
))
308 self
.model
.end_date
= str(self
.end_date
.date().toString(fmt
))
310 self
.results
= engineclass(self
.model
).search()
312 self
.display_results()
314 self
.commit_list
.clear()
315 self
.commit_text
.setText('')
317 def browse_callback(self
):
318 paths
= QtGui
.QFileDialog
.getOpenFileNames(self
,
319 N_('Choose Path(s)'))
323 lenprefix
= len(os
.getcwd()) + 1
324 for path
in map(lambda x
: unicode(x
), paths
):
325 if not path
.startswith(os
.getcwd()):
327 filepaths
.append(path
[lenprefix
:])
328 query
= subprocess
.list2cmdline(filepaths
)
329 self
.query
.setText(query
)
331 self
.search_callback()
333 def display_results(self
):
334 commit_list
= map(lambda x
: x
[1], self
.results
)
335 self
.set_commit_list(commit_list
)
337 def display(self
, *args
):
338 widget
= self
.commit_list
339 row
, selected
= qtutils
.selected_row(widget
)
340 if not selected
or len(self
.results
) < row
:
341 self
.commit_text
.setText('')
343 revision
= self
.results
[row
][0]
344 qtutils
.set_clipboard(revision
)
345 diff
= gitcmds
.commit_diff(revision
)
346 self
.commit_text
.setText(diff
)
348 def export_patch(self
):
349 widget
= self
.commit_list
350 row
, selected
= qtutils
.selected_row(widget
)
351 if not selected
or len(self
.results
) < row
:
353 revision
= self
.results
[row
][0]
354 Interaction
.log_status(*self
.model
.export_patchset(revision
, revision
))
356 def cherry_pick(self
):
357 widget
= self
.commit_list
358 row
, selected
= qtutils
.selected_row(widget
)
359 if not selected
or len(self
.results
) < row
:
361 revision
= self
.results
[row
][0]
362 Interaction
.log_status(*git
.cherry_pick(revision
,
366 def search_commits(parent
):
367 opts
= SearchOptions()
368 widget
= Search(opts
, parent
)
374 if __name__
== '__main__':
376 app
= QtGui
.QApplication(sys
.argv
)
379 sys
.exit(app
.exec_())