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
25 from cola
.compat
import ustr
29 return '%04d-%02d-%02d' % time
.localtime(timespec
)[:3]
32 class SearchOptions(object):
40 class SearchWidget(standard
.Dialog
):
41 def __init__(self
, parent
):
42 standard
.Dialog
.__init
__(self
, parent
)
43 self
.setAttribute(Qt
.WA_MacMetalStyle
)
44 self
.setWindowTitle(N_('Search'))
46 self
.mode_combo
= QtGui
.QComboBox()
47 self
.browse_button
= create_toolbutton(icon
=icons
.folder(),
48 tooltip
=N_('Browse...'))
49 self
.query
= QtGui
.QLineEdit()
51 self
.start_date
= QtGui
.QDateEdit()
52 self
.start_date
.setCurrentSection(QtGui
.QDateTimeEdit
.YearSection
)
53 self
.start_date
.setCalendarPopup(True)
54 self
.start_date
.setDisplayFormat(N_('yyyy-MM-dd'))
56 self
.end_date
= QtGui
.QDateEdit()
57 self
.end_date
.setCurrentSection(QtGui
.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)
65 self
.max_count
= QtGui
.QSpinBox()
66 self
.max_count
.setMinimum(5)
67 self
.max_count
.setMaximum(9995)
68 self
.max_count
.setSingleStep(5)
69 self
.max_count
.setValue(500)
71 self
.commit_list
= QtGui
.QListWidget()
72 self
.commit_list
.setMinimumSize(QtCore
.QSize(1, 1))
73 self
.commit_list
.setAlternatingRowColors(True)
74 selection_mode
= QtGui
.QAbstractItemView
.SingleSelection
75 self
.commit_list
.setSelectionMode(selection_mode
)
77 self
.commit_text
= DiffTextEdit(self
, whitespace
=False)
79 self
.button_export
= qtutils
.create_button(text
=N_('Export Patches'),
82 self
.button_cherrypick
= qtutils
.create_button(text
=N_('Cherry Pick'),
84 self
.button_close
= qtutils
.close_button()
86 self
.top_layout
= qtutils
.hbox(defs
.no_margin
, defs
.button_spacing
,
87 self
.query
, self
.start_date
,
88 self
.end_date
, self
.browse_button
,
89 self
.search_button
, qtutils
.STRETCH
,
90 self
.mode_combo
, self
.max_count
)
92 self
.splitter
= qtutils
.splitter(Qt
.Vertical
,
93 self
.commit_list
, self
.commit_text
)
95 self
.bottom_layout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
,
97 self
.button_cherrypick
,
98 qtutils
.STRETCH
, self
.button_close
)
100 self
.main_layout
= qtutils
.vbox(defs
.margin
, defs
.spacing
,
101 self
.top_layout
, self
.splitter
,
103 self
.setLayout(self
.main_layout
)
106 self
.resize(self
.parent().width(), self
.parent().height())
108 self
.resize(720, 500)
112 """Return a callback to handle various search actions."""
113 return search_commits(qtutils
.active_window())
116 class SearchEngine(object):
117 def __init__(self
, model
):
121 max_count
= self
.model
.max_count
124 'max-count': max_count
,
125 'pretty': 'format:%H %aN - %s - %ar',
128 def common_args(self
):
129 return (self
.model
.query
, self
.rev_args())
132 if not self
.validate():
134 return self
.results()
137 return len(self
.model
.query
) > 1
139 def revisions(self
, *args
, **kwargs
):
140 revlist
= git
.log(*args
, **kwargs
)[STDOUT
]
141 return gitcmds
.parse_rev_list(revlist
)
146 class RevisionSearch(SearchEngine
):
148 query
, opts
= self
.common_args()
149 args
= utils
.shell_split(query
)
150 return self
.revisions(all
=True, *args
, **opts
)
153 class PathSearch(SearchEngine
):
155 query
, args
= self
.common_args()
156 paths
= ['--'] + utils
.shell_split(query
)
157 return self
.revisions(all
=True, *paths
, **args
)
160 class MessageSearch(SearchEngine
):
162 query
, kwargs
= self
.common_args()
163 return self
.revisions(all
=True, grep
=query
, **kwargs
)
166 class AuthorSearch(SearchEngine
):
168 query
, kwargs
= self
.common_args()
169 return self
.revisions(all
=True, author
=query
, **kwargs
)
172 class CommitterSearch(SearchEngine
):
174 query
, kwargs
= self
.common_args()
175 return self
.revisions(all
=True, committer
=query
, **kwargs
)
178 class DiffSearch(SearchEngine
):
180 query
, kwargs
= self
.common_args()
181 return gitcmds
.parse_rev_list(
182 git
.log('-S'+query
, all
=True, **kwargs
)[STDOUT
])
185 class DateRangeSearch(SearchEngine
):
187 return self
.model
.start_date
< self
.model
.end_date
190 kwargs
= self
.rev_args()
191 start_date
= self
.model
.start_date
192 end_date
= self
.model
.end_date
193 return self
.revisions(date
='iso',
200 class Search(SearchWidget
):
202 def __init__(self
, model
, parent
):
203 SearchWidget
.__init
__(self
, parent
)
206 self
.EXPR
= N_('Search by Expression')
207 self
.PATH
= N_('Search by Path')
208 self
.MESSAGE
= N_('Search Commit Messages')
209 self
.DIFF
= N_('Search Diffs')
210 self
.AUTHOR
= N_('Search Authors')
211 self
.COMMITTER
= N_('Search Committers')
212 self
.DATE_RANGE
= N_('Search Date Range')
214 # Each search type is handled by a distinct SearchEngine subclass
216 self
.EXPR
: RevisionSearch
,
217 self
.PATH
: PathSearch
,
218 self
.MESSAGE
: MessageSearch
,
219 self
.DIFF
: DiffSearch
,
220 self
.AUTHOR
: AuthorSearch
,
221 self
.COMMITTER
: CommitterSearch
,
222 self
.DATE_RANGE
: DateRangeSearch
,
225 self
.modes
= (self
.EXPR
, self
.PATH
, self
.DATE_RANGE
,
226 self
.DIFF
, self
.MESSAGE
, self
.AUTHOR
, self
.COMMITTER
)
227 self
.mode_combo
.addItems(self
.modes
)
229 connect_button(self
.search_button
, self
.search_callback
)
230 connect_button(self
.browse_button
, self
.browse_callback
)
231 connect_button(self
.button_export
, self
.export_patch
)
232 connect_button(self
.button_cherrypick
, self
.cherry_pick
)
233 connect_button(self
.button_close
, self
.accept
)
235 self
.connect(self
.mode_combo
, SIGNAL('currentIndexChanged(int)'),
236 self
.mode_index_changed
)
238 self
.connect(self
.commit_list
,
239 SIGNAL('itemSelectionChanged()'),
242 self
.set_start_date(mkdate(time
.time()-(87640*31)))
243 self
.set_end_date(mkdate(time
.time()+87640))
244 self
.set_mode(self
.EXPR
)
246 self
.query
.setFocus()
248 def mode_index_changed(self
, idx
):
250 self
.update_shown_widgets(mode
)
251 if mode
== self
.PATH
:
252 self
.browse_callback()
254 def set_commit_list(self
, commits
):
255 widget
= self
.commit_list
257 widget
.addItems(commits
)
259 def set_start_date(self
, datestr
):
260 self
.set_date(self
.start_date
, datestr
)
262 def set_end_date(self
, datestr
):
263 self
.set_date(self
.end_date
, datestr
)
265 def set_date(self
, widget
, datestr
):
267 date
= QtCore
.QDate
.fromString(datestr
, fmt
)
271 def set_mode(self
, mode
):
272 idx
= self
.modes
.index(mode
)
273 self
.mode_combo
.setCurrentIndex(idx
)
274 self
.update_shown_widgets(mode
)
276 def update_shown_widgets(self
, mode
):
277 date_shown
= mode
== self
.DATE_RANGE
278 browse_shown
= mode
== self
.PATH
279 self
.query
.setVisible(not date_shown
)
280 self
.browse_button
.setVisible(browse_shown
)
281 self
.start_date
.setVisible(date_shown
)
282 self
.end_date
.setVisible(date_shown
)
285 return ustr(self
.mode_combo
.currentText())
287 def search_callback(self
, *args
):
288 engineclass
= self
.engines
[self
.mode()]
289 self
.model
.query
= ustr(self
.query
.text())
290 self
.model
.max_count
= self
.max_count
.value()
293 self
.model
.start_date
= str(self
.start_date
.date().toString(fmt
))
294 self
.model
.end_date
= str(self
.end_date
.date().toString(fmt
))
296 self
.results
= engineclass(self
.model
).search()
298 self
.display_results()
300 self
.commit_list
.clear()
301 self
.commit_text
.setText('')
303 def browse_callback(self
):
304 paths
= QtGui
.QFileDialog
.getOpenFileNames(self
,
305 N_('Choose Path(s)'))
309 lenprefix
= len(core
.getcwd()) + 1
310 for path
in map(lambda x
: ustr(x
), paths
):
311 if not path
.startswith(core
.getcwd()):
313 filepaths
.append(path
[lenprefix
:])
314 query
= core
.list2cmdline(filepaths
)
315 self
.query
.setText(query
)
317 self
.search_callback()
319 def display_results(self
):
320 commit_list
= [result
[1] for result
in self
.results
]
321 self
.set_commit_list(commit_list
)
323 def selected_revision(self
):
324 result
= qtutils
.selected_item(self
.commit_list
, self
.results
)
330 def display(self
, *args
):
331 revision
= self
.selected_revision()
333 self
.commit_text
.setText('')
335 qtutils
.set_clipboard(revision
)
336 diff
= gitcmds
.commit_diff(revision
)
337 self
.commit_text
.setText(diff
)
339 def export_patch(self
):
340 revision
= self
.selected_revision()
341 if revision
is not None:
342 Interaction
.log_status(*gitcmds
.export_patchset(revision
,
345 def cherry_pick(self
):
346 revision
= self
.selected_revision()
347 if revision
is not None:
348 Interaction
.log_status(*git
.cherry_pick(revision
))
351 def search_commits(parent
):
352 opts
= SearchOptions()
353 widget
= Search(opts
, parent
)
359 if __name__
== '__main__':
361 app
= QtGui
.QApplication(sys
.argv
)
364 sys
.exit(app
.exec_())