1"""A widget for searching git commits""" 2from __future__ import division, absolute_import, unicode_literals 3import time 4 5from qtpy import QtCore 6from qtpy import QtWidgets 7from qtpy.QtCore import Qt 8 9from ..i18n import N_ 10from ..interaction import Interaction 11from ..git import STDOUT 12from ..qtutils import connect_button 13from ..qtutils import create_toolbutton 14from ..qtutils import get 15from .. import core 16from .. import gitcmds 17from .. import icons 18from .. import utils 19from .. import qtutils 20from . import diff 21from . import defs 22from . import standard 23 24 25def mkdate(timespec): 26 return '%04d-%02d-%02d' % time.localtime(timespec)[:3] 27 28 29class SearchOptions(object): 30 def __init__(self): 31 self.query = '' 32 self.max_count = 500 33 self.start_date = '' 34 self.end_date = '' 35 36 37class SearchWidget(standard.Dialog): 38 def __init__(self, context, parent): 39 standard.Dialog.__init__(self, parent) 40 41 self.context = context 42 self.setWindowTitle(N_('Search')) 43 44 self.mode_combo = QtWidgets.QComboBox() 45 self.browse_button = create_toolbutton( 46 icon=icons.folder(), tooltip=N_('Browse...') 47 ) 48 self.query = QtWidgets.QLineEdit() 49 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')) 54 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')) 59 60 icon = icons.search() 61 self.search_button = qtutils.create_button( 62 text=N_('Search'), icon=icon, default=True 63 ) 64 self.max_count = standard.SpinBox(value=500, mini=5, maxi=9995, step=5) 65 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) 71 72 self.commit_text = diff.DiffTextEdit(context, self, whitespace=False) 73 74 self.button_export = qtutils.create_button( 75 text=N_('Export Patches'), icon=icons.diff() 76 ) 77 78 self.button_cherrypick = qtutils.create_button( 79 text=N_('Cherry Pick'), icon=icons.save() 80 ) 81 self.button_close = qtutils.close_button() 82 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, 94 ) 95 96 self.splitter = qtutils.splitter( 97 Qt.Vertical, self.commit_list, self.commit_text 98 ) 99 100 self.bottom_layout = qtutils.hbox( 101 defs.no_margin, 102 defs.spacing, 103 self.button_close, 104 qtutils.STRETCH, 105 self.button_export, 106 self.button_cherrypick, 107 ) 108 109 self.main_layout = qtutils.vbox( 110 defs.margin, 111 defs.spacing, 112 self.top_layout, 113 self.splitter, 114 self.bottom_layout, 115 ) 116 self.setLayout(self.main_layout) 117 118 self.init_size(parent=parent) 119 120 121def search(context): 122 """Return a callback to handle various search actions.""" 123 return search_commits(context, qtutils.active_window()) 124 125 126class SearchEngine(object): 127 def __init__(self, context, model): 128 self.context = context 129 self.model = model 130 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', 137 } 138 139 def common_args(self): 140 return (self.model.query, self.rev_args()) 141 142 def search(self): 143 if self.validate(): 144 return self.results() 145 return [] 146 147 def validate(self): 148 return len(self.model.query) > 1 149 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) 154 155 def results(self): 156 pass 157 158 159class RevisionSearch(SearchEngine): 160 def results(self): 161 query, opts = self.common_args() 162 args = utils.shell_split(query) 163 return self.revisions(*args, **opts) 164 165 166class 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) 171 172 173class MessageSearch(SearchEngine): 174 def results(self): 175 query, kwargs = self.common_args() 176 return self.revisions(all=True, grep=query, **kwargs) 177 178 179class AuthorSearch(SearchEngine): 180 def results(self): 181 query, kwargs = self.common_args() 182 return self.revisions(all=True, author=query, **kwargs) 183 184 185class CommitterSearch(SearchEngine): 186 def results(self): 187 query, kwargs = self.common_args() 188 return self.revisions(all=True, committer=query, **kwargs) 189 190 191class 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]) 196 197 198class DateRangeSearch(SearchEngine): 199 def validate(self): 200 return self.model.start_date < self.model.end_date 201 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 208 ) 209 210 211class Search(SearchWidget): 212 def __init__(self, context, model, parent): 213 """ 214 Search diffs and commit logs 215 216 :param model: SearchOptions instance 217 218 """ 219 SearchWidget.__init__(self, context, parent) 220 self.model = model 221 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 = [] 230 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, 240 } 241 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, 250 ) 251 self.mode_combo.addItems(self.modes) 252 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) 258 259 # pylint: disable=no-member 260 self.mode_combo.currentIndexChanged.connect(self.mode_changed) 261 self.commit_list.itemSelectionChanged.connect(self.display) 262 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) 266 267 self.query.setFocus() 268 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() 274 275 def set_commits(self, commits): 276 widget = self.commit_list 277 widget.clear() 278 widget.addItems(commits) 279 280 def set_start_date(self, datestr): 281 set_date(self.start_date, datestr) 282 283 def set_end_date(self, datestr): 284 set_date(self.end_date, datestr) 285 286 def set_mode(self, mode): 287 idx = self.modes.index(mode) 288 self.mode_combo.setCurrentIndex(idx) 289 self.update_shown_widgets(mode) 290 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) 298 299 def mode(self): 300 return self.mode_combo.currentText() 301 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) 307 308 self.model.start_date = get(self.start_date) 309 self.model.end_date = get(self.end_date) 310 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('') 317 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) 331 332 query = core.list2cmdline(filepaths) 333 self.query.setText(query) 334 if query: 335 self.search_callback() 336 337 def display_results(self): 338 commits = [result[1] for result in self.results] 339 self.set_commits(commits) 340 341 def selected_revision(self): 342 result = qtutils.selected_item(self.commit_list, self.results) 343 return result[0] if result else None 344 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) 355 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) 362 ) 363 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)) 369 370 371def set_date(widget, datestr): 372 fmt = Qt.ISODate 373 date = QtCore.QDate.fromString(datestr, fmt) 374 if date: 375 widget.setDate(date) 376 377 378def search_commits(context, parent): 379 opts = SearchOptions() 380 widget = Search(context, opts, parent) 381 widget.show() 382 return widget 383