controllers: implement search by commit message
[ugit.git] / ugit / models.py
blob386c197dc1311bef2c95b224b946529a917911f6
1 import os
2 import re
4 from ugit import git
5 from ugit import utils
6 from ugit import model
8 class Model(model.Model):
9 """Provides a friendly wrapper for doing commit git operations."""
11 def __init__(self):
12 """Reads git repository settings and sets severl methods
13 so that they refer to the git module. This object is
14 encapsulates ugit's interaction with git.
15 The git module itself should know nothing about ugit
16 whatsoever."""
18 model.Model.__init__(self)
19 # chdir to the root of the git tree.
20 # This keeps paths relative.
21 cdup = git.rev_parse(show_cdup=True)
22 if cdup:
23 os.chdir(cdup)
25 # Read git config
26 self.init_config_data()
28 # These methods are best left implemented in git.py
29 for cmd in (
30 'add',
31 'add_or_remove',
32 'cat_file',
33 'checkout',
34 'create_branch',
35 'cherry_pick',
36 'cherry_pick_list',
37 'commit_with_msg',
38 'diff',
39 'diff_helper',
40 'diffstat',
41 'diffindex',
42 'format_patch',
43 'format_patch_helper',
44 'push',
45 'push_helper',
46 'show',
47 'log',
48 'log_helper',
49 'rebase',
50 'remote_url',
51 'rev_list',
52 'rev_list_range',
53 'parse_rev_list',
54 'parsed_rev_range',
56 setattr(self, cmd, getattr(git,cmd))
58 self.create(
59 #####################################################
60 # Used in various places
61 branch = '',
62 remotes = [],
63 remote = '',
64 local_branch = '',
65 remote_branch = '',
66 search_text = '',
67 git_version = git.version(),
69 #####################################################
70 # Used primarily by the main UI
71 project = os.path.basename(os.getcwd()),
72 commitmsg = '',
73 modified = [],
74 staged = [],
75 unstaged = [],
76 untracked = [],
77 window_geom = utils.parse_geom(
78 self.get_global_ugit_geometry()),
80 #####################################################
81 # Used by the create branch dialog
82 revision = '',
83 local_branches = [],
84 remote_branches = [],
85 tags = [],
87 #####################################################
88 # Used by the commit/repo browser
89 directory = '',
90 revisions = [],
91 summaries = [],
93 # These are parallel lists
94 types = [],
95 sha1s = [],
96 names = [],
98 # All items below here are re-calculated in
99 # init_browser_data()
100 directories = [],
101 directory_entries = {},
103 # These are also parallel lists
104 subtree_types = [],
105 subtree_sha1s = [],
106 subtree_names = [],
110 def init_config_data(self):
111 """Reads git config --list and creates parameters
112 for each setting."""
113 # These parameters are saved in .gitconfig,
114 # so ideally these should be as short as possible.
116 # config items that are controllable globally
117 # and per-repository
118 self.__local_and_global_defaults = {
119 'user_name': '',
120 'user_email': '',
121 'merge_summary': False,
122 'merge_diffstat': True,
123 'merge_verbosity': 2,
124 'gui_diffcontext': 5,
125 'gui_pruneduringfetch': False,
127 # config items that are purely git config --global settings
128 self.__global_defaults = {
129 'ugit_geometry':'',
130 'ugit_fontui': '',
131 'ugit_fontui_size':12,
132 'ugit_fontdiff': '',
133 'ugit_fontdiff_size':12,
134 'ugit_historybrowser': 'gitk',
135 'ugit_savewindowsettings': False,
136 'ugit_saveatexit': False,
139 local_dict = git.config_dict(local=True)
140 global_dict = git.config_dict(local=False)
142 for k,v in local_dict.iteritems():
143 self.set_param('local_'+k, v)
144 for k,v in global_dict.iteritems():
145 self.set_param('global_'+k, v)
146 if k not in local_dict:
147 local_dict[k]=v
148 self.set_param('local_'+k, v)
150 # Bootstrap the internal font*_size variables
151 for param in ('global_ugit_fontui', 'global_ugit_fontdiff'):
152 if hasattr(self, param):
153 font = self.get_param(param)
154 if font:
155 size = int(font.split(',')[1])
156 self.set_param(param+'_size', size)
157 param = param[len('global_'):]
158 global_dict[param] = font
159 global_dict[param+'_size'] = size
161 # Load defaults for all undefined items
162 local_and_global_defaults = self.__local_and_global_defaults
163 for k,v in local_and_global_defaults.iteritems():
164 if k not in local_dict:
165 self.set_param('local_'+k, v)
166 if k not in global_dict:
167 self.set_param('global_'+k, v)
169 global_defaults = self.__global_defaults
170 for k,v in global_defaults.iteritems():
171 if k not in global_dict:
172 self.set_param('global_'+k, v)
174 def save_config_param(self, param):
175 if param not in self.get_config_params():
176 return
177 value = self.get_param(param)
178 if param == 'local_gui_diffcontext':
179 git.DIFF_CONTEXT = value
180 if param.startswith('local_'):
181 param = param[len('local_'):]
182 is_local = True
183 elif param.startswith('global_'):
184 param = param[len('global_'):]
185 is_local = False
186 else:
187 raise Exception("Invalid param '%s' passed to " % param
188 + "save_config_param()")
189 param = param.replace('_','.') # model -> git
190 return git.config_set(param, value, local=is_local)
192 def init_browser_data(self):
193 '''This scans over self.(names, sha1s, types) to generate
194 directories, directory_entries, and subtree_*'''
196 # Collect data for the model
197 if not self.get_branch(): return
199 self.subtree_types = []
200 self.subtree_sha1s = []
201 self.subtree_names = []
202 self.directories = []
203 self.directory_entries = {}
205 # Lookup the tree info
206 tree_info = git.parse_ls_tree(self.get_branch())
208 self.set_types(map( lambda(x): x[1], tree_info ))
209 self.set_sha1s(map( lambda(x): x[2], tree_info ))
210 self.set_names(map( lambda(x): x[3], tree_info ))
212 if self.directory: self.directories.append('..')
214 dir_entries = self.directory_entries
215 dir_regex = re.compile('([^/]+)/')
216 dirs_seen = {}
217 subdirs_seen = {}
219 for idx, name in enumerate(self.names):
221 if not name.startswith(self.directory): continue
222 name = name[ len(self.directory): ]
224 if name.count('/'):
225 # This is a directory...
226 match = dir_regex.match(name)
227 if not match: continue
229 dirent = match.group(1) + '/'
230 if dirent not in self.directory_entries:
231 self.directory_entries[dirent] = []
233 if dirent not in dirs_seen:
234 dirs_seen[dirent] = True
235 self.directories.append(dirent)
237 entry = name.replace(dirent, '')
238 entry_match = dir_regex.match(entry)
239 if entry_match:
240 subdir = entry_match.group(1) + '/'
241 if subdir in subdirs_seen: continue
242 subdirs_seen[subdir] = True
243 dir_entries[dirent].append(subdir)
244 else:
245 dir_entries[dirent].append(entry)
246 else:
247 self.subtree_types.append(self.types[idx])
248 self.subtree_sha1s.append(self.sha1s[idx])
249 self.subtree_names.append(name)
251 def get_history_browser(self):
252 return self.get_param('global_ugit_historybrowser')
254 def remember_gui_settings(self):
255 return self.get_param('global_ugit_savewindowsettings')
257 def save_at_exit(self):
258 return self.get_param('global_ugit_saveatexit')
260 def get_tree_node(self, idx):
261 return (self.get_types()[idx],
262 self.get_sha1s()[idx],
263 self.get_names()[idx] )
265 def get_subtree_node(self, idx):
266 return (self.get_subtree_types()[idx],
267 self.get_subtree_sha1s()[idx],
268 self.get_subtree_names()[idx] )
270 def get_all_branches(self):
271 return (self.get_local_branches() + self.get_remote_branches())
273 def set_remote(self, remote):
274 if not remote: return
275 self.set_param('remote', remote)
276 branches = utils.grep( '%s/\S+$' % remote,
277 git.branch(remote=True), squash=False)
278 self.set_remote_branches(branches)
280 def add_signoff(self,*rest):
281 '''Adds a standard Signed-off by: tag to the end
282 of the current commit message.'''
284 msg = self.get_commitmsg()
285 signoff =('\n\nSigned-off-by: %s <%s>\n' % (
286 self.get_local_user_name(),
287 self.get_local_user_email()))
289 if signoff not in msg:
290 self.set_commitmsg(msg + signoff)
292 def apply_diff(self, filename):
293 return git.apply(filename, index=True, cached=True)
295 def __get_squash_msg_path(self):
296 return os.path.join(os.getcwd(), '.git', 'SQUASH_MSG')
298 def has_squash_msg(self):
299 squash_msg = self.__get_squash_msg_path()
300 return os.path.exists(squash_msg)
302 def get_squash_msg(self):
303 return utils.slurp(self.__get_squash_msg_path())
305 def set_squash_msg(self):
306 self.set_commitmsg(self.get_squash_msg())
308 def get_prev_commitmsg(self,*rest):
309 '''Queries git for the latest commit message and sets it in
310 self.commitmsg.'''
311 commit_msg = []
312 commit_lines = git.show('HEAD').split('\n')
313 for idx, msg in enumerate(commit_lines):
314 if idx < 4: continue
315 msg = msg.lstrip()
316 if msg.startswith('diff --git'):
317 commit_msg.pop()
318 break
319 commit_msg.append(msg)
320 self.set_commitmsg('\n'.join(commit_msg).rstrip())
322 def update_status(self):
323 # This allows us to defer notification until the
324 # we finish processing data
325 notify_enabled = self.get_notify()
326 self.set_notify(False)
328 # Reset the staged and unstaged model lists
329 # NOTE: the model's unstaged list is used to
330 # hold both modified and untracked files.
331 self.staged = []
332 self.modified = []
333 self.untracked = []
335 # Read git status items
336 ( staged_items,
337 modified_items,
338 untracked_items ) = git.parse_status()
340 # Gather items to be committed
341 for staged in staged_items:
342 if staged not in self.get_staged():
343 self.add_staged(staged)
345 # Gather unindexed items
346 for modified in modified_items:
347 if modified not in self.get_modified():
348 self.add_modified(modified)
350 # Gather untracked items
351 for untracked in untracked_items:
352 if untracked not in self.get_untracked():
353 self.add_untracked(untracked)
355 self.set_branch(git.current_branch())
356 self.set_unstaged(self.get_modified() + self.get_untracked())
357 self.set_remotes(git.remote().splitlines())
358 self.set_remote_branches(git.branch(remote=True))
359 self.set_local_branches(git.branch(remote=False))
360 self.set_tags(git.tag().splitlines())
361 self.set_revision('')
362 self.set_local_branch('')
363 self.set_remote_branch('')
364 # Re-enable notifications and emit changes
365 self.set_notify(notify_enabled)
366 self.notify_observers('staged','unstaged')
368 def delete_branch(self, branch):
369 return git.branch(name=branch, delete=True)
371 def get_revision_sha1(self, idx):
372 return self.get_revisions()[idx]
374 def get_config_params(self):
375 params = []
376 params.extend(map(lambda x: 'local_' + x,
377 self.__local_and_global_defaults.keys()))
378 params.extend(map(lambda x: 'global_' + x,
379 self.__local_and_global_defaults.keys()))
380 params.extend(map(lambda x: 'global_' + x,
381 self.__global_defaults.keys()))
382 return params
384 def apply_font_size(self, param, default):
385 old_font = self.get_param(param)
386 if not old_font:
387 old_font = default
389 size = self.get_param(param+'_size')
390 props = old_font.split(',')
391 props[1] = str(size)
392 new_font = ','.join(props)
394 self.set_param(param, new_font)
396 def read_font_size(self, param, new_font):
397 new_size = int(new_font.split(',')[1])
398 self.set_param(param, new_size)
400 def get_commit_diff(self, sha1):
401 commit = git.show(sha1)
402 first_newline = commit.index('\n')
403 if commit[first_newline+1:].startswith('Merge:'):
404 return (commit
405 + '\n\n'
406 + self.diff_helper(
407 commit=sha1,
408 cached=False,
409 suppress_header=False,
412 else:
413 return commit
415 def get_diff_and_status(self, idx, staged=True):
416 if staged:
417 filename = self.get_staged()[idx]
418 if os.path.exists(filename):
419 status = 'Staged for commit'
420 else:
421 status = 'Staged for removal'
422 diff = self.diff_helper(
423 filename=filename,
424 cached=True,
426 else:
427 filename = self.get_unstaged()[idx]
428 if os.path.isdir(filename):
429 status = 'Untracked directory'
430 diff = '\n'.join(os.listdir(filename))
431 elif filename in self.get_modified():
432 status = 'Modified, not staged'
433 diff = self.diff_helper(
434 filename=filename,
435 cached=False,
437 else:
438 status = 'Untracked, not staged'
440 file_type = utils.run_cmd('file',filename, b=True)
441 if 'binary' in file_type or 'data' in file_type:
442 diff = utils.run_cmd('hexdump', filename, C=True)
443 else:
444 if os.path.exists(filename):
445 file = open(filename, 'r')
446 diff = file.read()
447 file.close()
448 else:
449 diff = ''
450 return diff, status
452 def stage_modified(self):
453 output = git.add(self.get_modified())
454 self.update_status()
455 return output
457 def stage_untracked(self):
458 output = git.add(self.get_untracked())
459 self.update_status()
460 return output
462 def reset(self, *items):
463 output = git.reset('--', *items)
464 self.update_status()
465 return output
467 def unstage_all(self):
468 git.reset('--', *self.get_staged())
469 self.update_status()
471 def save_gui_settings(self):
472 git.config_set('ugit.geometry', utils.get_geom(), local=False)