views.actions: Add a command wrapper for run_command and use it
[git-cola.git] / cola / controllers / search.py
blobd769f4ca0dce1a29f9b89a08b51a46bcda166259
1 """This controller handles the search dialog."""
4 import os
5 import re
6 import time
7 from PyQt4 import QtGui
9 import cola
10 from cola import gitcmds
11 from cola import qtutils
12 from cola.qobserver import QObserver
13 from cola.models.search import SearchModel
14 from cola.views.search import SearchView
16 # Modes for this controller.
17 # Note: names correspond to radio button names for convenience
18 REVISION_ID = 'radio_revision'
19 REVISION_RANGE = 'radio_range'
20 PATH = 'radio_path'
21 MESSAGE = 'radio_message'
22 DIFF = 'radio_diff'
23 AUTHOR = 'radio_author'
24 COMMITTER = 'radio_committer'
25 DATE_RANGE = 'radio_daterange'
28 def search(searchtype, browse=False):
29 """Return a callback to handle various search actions."""
30 def search_handler():
31 search_commits(cola.model(),
32 QtGui.QApplication.instance().activeWindow(),
33 searchtype,
34 browse)
35 return search_handler
38 class SearchEngine(object):
39 def __init__(self, model):
40 self.model = model
42 def rev_args(self):
43 max_count = self.model.max_results
44 return {
45 'no_color': True,
46 'max-count': max_count,
47 'pretty': 'format:%H %aN - %s - %ar',
50 def common_args(self):
51 return (self.model.query, self.rev_args())
53 def search(self):
54 if not self.validate():
55 return
56 return self.results()
58 def validate(self):
59 return len(self.model.query) > 1
61 def revisions(self, *args, **kwargs):
62 revlist = self.model.git.log(*args, **kwargs)
63 return gitcmds.parse_rev_list(revlist)
65 def results(self):
66 pass
68 class RevisionSearch(SearchEngine):
69 def results(self):
70 query, args = self.common_args()
71 expr = re.compile(query)
72 revs = self.revisions(all=True, **args)
73 return [ r for r in revs if expr.match(r[0]) ]
75 class RevisionRangeSearch(SearchEngine):
76 def __init__(self, model):
77 SearchEngine.__init__(self, model)
78 self.RE = re.compile(r'[^.]*\.\..*')
79 def validate(self):
80 return bool(self.RE.match(self.model.query))
81 def results(self):
82 query, kwargs = self.common_args()
83 return self.revisions(query, **kwargs)
85 class PathSearch(SearchEngine):
86 def results(self):
87 query, args = self.common_args()
88 paths = ['--'] + query.split(':')
89 return self.revisions(all=True, *paths, **args)
91 class MessageSearch(SearchEngine):
92 def results(self):
93 query, kwargs = self.common_args()
94 return self.revisions(all=True, grep=query, **kwargs)
96 class AuthorSearch(SearchEngine):
97 def results(self):
98 query, kwargs = self.common_args()
99 return self.revisions(all=True, author=query, **kwargs)
101 class CommitterSearch(SearchEngine):
102 def results(self):
103 query, kwargs = self.common_args()
104 return self.revisions(all=True, committer=query, **kwargs)
106 class DiffSearch(SearchEngine):
107 def results(self):
108 query, kwargs = self.common_args()
109 return gitcmds.parse_rev_list(
110 self.model.git.log('-S'+query, all=True, **kwargs))
112 class DateRangeSearch(SearchEngine):
113 def validate(self):
114 return True
115 def results(self):
116 kwargs = self.rev_args()
117 start_date = self.model.start_date
118 end_date = self.model.end_date
119 return self.revisions(date='iso',
120 all=True,
121 after=start_date,
122 before=end_date,
123 **kwargs)
125 # Each search type is handled by a distinct SearchEngine subclass
126 SEARCH_ENGINES = {
127 REVISION_ID: RevisionSearch,
128 REVISION_RANGE: RevisionRangeSearch,
129 PATH: PathSearch,
130 MESSAGE: MessageSearch,
131 DIFF: DiffSearch,
132 AUTHOR: AuthorSearch,
133 COMMITTER: CommitterSearch,
134 DATE_RANGE: DateRangeSearch,
137 class SearchController(QObserver):
138 def __init__(self, model, view):
139 QObserver.__init__(self, model, view)
140 self.add_observables('query',
141 'max_results',
142 'start_date',
143 'end_date')
144 self.add_actions(max_results = self.search_callback,
145 start_date = self.search_callback,
146 end_date = self.search_callback)
147 self.add_callbacks(
148 # Standard buttons
149 button_search = self.search_callback,
150 button_browse = self.browse_callback,
151 commit_list = self.display_callback,
152 button_export = self.export_patch,
153 button_cherrypick = self.cherry_pick,
155 # Radio buttons trigger a search
156 radio_revision = self.search_callback,
157 radio_range = self.search_callback,
158 radio_message = self.search_callback,
159 radio_path = self.search_callback,
160 radio_diff = self.search_callback,
161 radio_author = self.search_callback,
162 radio_committer = self.search_callback,
163 radio_daterange = self.search_callback,
165 self.update_fonts()
167 def update_fonts(self):
168 font = self.model.cola_config('fontdiff')
169 if font:
170 qfont = QtGui.QFont()
171 qfont.fromString(font)
172 self.view.commit_text.setFont(qfont)
174 def set_mode(self, mode):
175 radio = getattr(self.view, mode)
176 radio.setChecked(True)
178 def radio_to_mode(self, radio_button):
179 return str(radio_button.objectName())
181 def mode(self):
182 for name in SEARCH_ENGINES:
183 radiobutton = getattr(self.view, name)
184 if radiobutton.isChecked():
185 return name
187 def search_callback(self, *args):
188 engineclass = SEARCH_ENGINES.get(self.mode())
189 if not engineclass:
190 print "mode: '%s' is currently unimplemented" % self.mode()
191 return
192 self.results = engineclass(self.model).search()
193 if self.results:
194 self.display_results()
195 else:
196 self.view.commit_list.clear()
197 self.view.commit_text.setText('')
199 def browse_callback(self):
200 paths = QtGui.QFileDialog.getOpenFileNames(self.view,
201 self.tr("Choose Path(s)"))
202 if not paths:
203 return
204 filepaths = []
205 lenprefix = len(os.getcwd()) + 1
206 for path in map(lambda x: unicode(x), paths):
207 if not path.startswith(os.getcwd()):
208 continue
209 filepaths.append(path[lenprefix:])
210 query = ':'.join(filepaths)
211 self.model.set_query('')
212 self.set_mode(PATH)
213 self.model.set_query(query)
215 def display_results(self):
216 commit_list = map(lambda x: x[1], self.results)
217 self.model.set_commit_list(commit_list)
218 qtutils.set_listwidget_strings(self.view.commit_list, commit_list)
220 def display_callback(self, *args):
221 widget = self.view.commit_list
222 row, selected = qtutils.selected_row(widget)
223 if not selected or len(self.results) < row:
224 return
225 revision = self.results[row][0]
226 qtutils.set_clipboard(revision)
227 diff = gitcmds.commit_diff(revision)
228 self.view.commit_text.setText(diff)
230 def export_patch(self):
231 widget = self.view.commit_list
232 row, selected = qtutils.selected_row(widget)
233 if not selected or len(self.results) < row:
234 return
235 revision = self.results[row][0]
236 qtutils.log(*self.model.export_patchset(revision, revision))
238 def cherry_pick(self):
239 widget = self.view.commit_list
240 row, selected = qtutils.selected_row(widget)
241 if not selected or len(self.results) < row:
242 return
243 revision = self.results[row][0]
244 qtutils.log(*self.model.git.cherry_pick(revision,
245 with_stderr=True,
246 with_status=True))
248 def search_commits(model, parent, mode, browse):
249 def date(timespec):
250 return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
252 # TODO subclass model for search only
253 model = SearchModel(cwd=model.git.worktree())
254 view = SearchView(parent)
255 ctl = SearchController(model, view)
256 ctl.set_mode(mode)
257 model.set_start_date(date(time.time()-(87640*7)))
258 model.set_end_date(date(time.time()+87640))
259 view.show()
260 if browse:
261 ctl.browse_callback()