layout: move views and controllers into a subdirectory
[ugit.git] / ugit / models.py
blobbea8deab22b976d4a55b65cf124e5194bf2c8e9e
1 import os
2 import re
4 import git
5 import utils
6 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_range',
53 setattr(self, cmd, getattr(git,cmd))
55 self.create(
56 #####################################################
57 # Used in various places
58 branch = '',
59 remotes = [],
60 remote = '',
61 local_branch = '',
62 remote_branch = '',
63 search_text = '',
64 git_version = git.version(),
66 #####################################################
67 # Used primarily by the main UI
68 project = os.path.basename(os.getcwd()),
69 commitmsg = '',
70 modified = [],
71 staged = [],
72 unstaged = [],
73 untracked = [],
74 window_geom = utils.parse_geom(
75 self.get_global_ugit_geometry()),
77 #####################################################
78 # Used by the create branch dialog
79 revision = '',
80 local_branches = [],
81 remote_branches = [],
82 tags = [],
84 #####################################################
85 # Used by the commit/repo browser
86 directory = '',
87 revisions = [],
88 summaries = [],
90 # These are parallel lists
91 types = [],
92 sha1s = [],
93 names = [],
95 # All items below here are re-calculated in
96 # init_browser_data()
97 directories = [],
98 directory_entries = {},
100 # These are also parallel lists
101 subtree_types = [],
102 subtree_sha1s = [],
103 subtree_names = [],
107 def init_config_data(self):
108 """Reads git config --list and creates parameters
109 for each setting."""
110 # These parameters are saved in .gitconfig,
111 # so ideally these should be as short as possible.
113 # config items that are controllable globally
114 # and per-repository
115 self.__local_and_global_defaults = {
116 'user_name': '',
117 'user_email': '',
118 'merge_summary': False,
119 'merge_diffstat': True,
120 'merge_verbosity': 2,
121 'gui_diffcontext': 5,
122 'gui_pruneduringfetch': False,
124 # config items that are purely git config --global settings
125 self.__global_defaults = {
126 'ugit_geometry':'',
127 'ugit_fontui': '',
128 'ugit_fontui_size':12,
129 'ugit_fontdiff': '',
130 'ugit_fontdiff_size':12,
131 'ugit_historybrowser': 'gitk',
132 'ugit_savewindowsettings': False,
133 'ugit_saveatexit': False,
136 local_dict = git.config_dict(local=True)
137 global_dict = git.config_dict(local=False)
139 for k,v in local_dict.iteritems():
140 self.set_param('local_'+k, v)
141 for k,v in global_dict.iteritems():
142 self.set_param('global_'+k, v)
143 if k not in local_dict:
144 local_dict[k]=v
145 self.set_param('local_'+k, v)
147 # Bootstrap the internal font*_size variables
148 for param in ('global_ugit_fontui', 'global_ugit_fontdiff'):
149 if hasattr(self, param):
150 font = self.get_param(param)
151 if font:
152 size = int(font.split(',')[1])
153 self.set_param(param+'_size', size)
154 param = param[len('global_'):]
155 global_dict[param] = font
156 global_dict[param+'_size'] = size
158 # Load defaults for all undefined items
159 local_and_global_defaults = self.__local_and_global_defaults
160 for k,v in local_and_global_defaults.iteritems():
161 if k not in local_dict:
162 self.set_param('local_'+k, v)
163 if k not in global_dict:
164 self.set_param('global_'+k, v)
166 global_defaults = self.__global_defaults
167 for k,v in global_defaults.iteritems():
168 if k not in global_dict:
169 self.set_param('global_'+k, v)
171 def save_config_param(self, param):
172 if param not in self.get_config_params():
173 return
174 value = self.get_param(param)
175 if param == 'local_gui_diffcontext':
176 git.DIFF_CONTEXT = value
177 if param.startswith('local_'):
178 param = param[len('local_'):]
179 is_local = True
180 elif param.startswith('global_'):
181 param = param[len('global_'):]
182 is_local = False
183 else:
184 raise Exception("Invalid param '%s' passed to " % param
185 + "save_config_param()")
186 param = param.replace('_','.') # model -> git
187 return git.config_set(param, value, local=is_local)
189 def init_browser_data(self):
190 '''This scans over self.(names, sha1s, types) to generate
191 directories, directory_entries, and subtree_*'''
193 # Collect data for the model
194 if not self.get_branch(): return
196 self.subtree_types = []
197 self.subtree_sha1s = []
198 self.subtree_names = []
199 self.directories = []
200 self.directory_entries = {}
202 # Lookup the tree info
203 tree_info = git.parse_ls_tree(self.get_branch())
205 self.set_types(map( lambda(x): x[1], tree_info ))
206 self.set_sha1s(map( lambda(x): x[2], tree_info ))
207 self.set_names(map( lambda(x): x[3], tree_info ))
209 if self.directory: self.directories.append('..')
211 dir_entries = self.directory_entries
212 dir_regex = re.compile('([^/]+)/')
213 dirs_seen = {}
214 subdirs_seen = {}
216 for idx, name in enumerate(self.names):
218 if not name.startswith(self.directory): continue
219 name = name[ len(self.directory): ]
221 if name.count('/'):
222 # This is a directory...
223 match = dir_regex.match(name)
224 if not match: continue
226 dirent = match.group(1) + '/'
227 if dirent not in self.directory_entries:
228 self.directory_entries[dirent] = []
230 if dirent not in dirs_seen:
231 dirs_seen[dirent] = True
232 self.directories.append(dirent)
234 entry = name.replace(dirent, '')
235 entry_match = dir_regex.match(entry)
236 if entry_match:
237 subdir = entry_match.group(1) + '/'
238 if subdir in subdirs_seen: continue
239 subdirs_seen[subdir] = True
240 dir_entries[dirent].append(subdir)
241 else:
242 dir_entries[dirent].append(entry)
243 else:
244 self.subtree_types.append(self.types[idx])
245 self.subtree_sha1s.append(self.sha1s[idx])
246 self.subtree_names.append(name)
248 def get_history_browser(self):
249 return self.get_param('global_ugit_historybrowser')
251 def remember_gui_settings(self):
252 return self.get_param('global_ugit_savewindowsettings')
254 def save_at_exit(self):
255 return self.get_param('global_ugit_saveatexit')
257 def get_tree_node(self, idx):
258 return (self.get_types()[idx],
259 self.get_sha1s()[idx],
260 self.get_names()[idx] )
262 def get_subtree_node(self, idx):
263 return (self.get_subtree_types()[idx],
264 self.get_subtree_sha1s()[idx],
265 self.get_subtree_names()[idx] )
267 def get_all_branches(self):
268 return (self.get_local_branches() + self.get_remote_branches())
270 def set_remote(self, remote):
271 if not remote: return
272 self.set_param('remote', remote)
273 branches = utils.grep( '%s/\S+$' % remote,
274 git.branch(remote=True), squash=False)
275 self.set_remote_branches(branches)
277 def add_signoff(self,*rest):
278 '''Adds a standard Signed-off by: tag to the end
279 of the current commit message.'''
281 msg = self.get_commitmsg()
282 signoff =('\n\nSigned-off-by: %s <%s>\n' % (
283 self.get_local_user_name(),
284 self.get_local_user_email()))
286 if signoff not in msg:
287 self.set_commitmsg(msg + signoff)
289 def apply_diff(self, filename):
290 return git.apply(filename, index=True, cached=True)
292 def __get_squash_msg_path(self):
293 return os.path.join(os.getcwd(), '.git', 'SQUASH_MSG')
295 def has_squash_msg(self):
296 squash_msg = self.__get_squash_msg_path()
297 return os.path.exists(squash_msg)
299 def get_squash_msg(self):
300 return utils.slurp(self.__get_squash_msg_path())
302 def set_squash_msg(self):
303 self.set_commitmsg(self.get_squash_msg())
305 def get_prev_commitmsg(self,*rest):
306 '''Queries git for the latest commit message and sets it in
307 self.commitmsg.'''
308 commit_msg = []
309 commit_lines = git.show('HEAD').split('\n')
310 for idx, msg in enumerate(commit_lines):
311 if idx < 4: continue
312 msg = msg.lstrip()
313 if msg.startswith('diff --git'):
314 commit_msg.pop()
315 break
316 commit_msg.append(msg)
317 self.set_commitmsg('\n'.join(commit_msg).rstrip())
319 def update_status(self):
320 # This allows us to defer notification until the
321 # we finish processing data
322 notify_enabled = self.get_notify()
323 self.set_notify(False)
325 # Reset the staged and unstaged model lists
326 # NOTE: the model's unstaged list is used to
327 # hold both modified and untracked files.
328 self.staged = []
329 self.modified = []
330 self.untracked = []
332 # Read git status items
333 ( staged_items,
334 modified_items,
335 untracked_items ) = git.parse_status()
337 # Gather items to be committed
338 for staged in staged_items:
339 if staged not in self.get_staged():
340 self.add_staged(staged)
342 # Gather unindexed items
343 for modified in modified_items:
344 if modified not in self.get_modified():
345 self.add_modified(modified)
347 # Gather untracked items
348 for untracked in untracked_items:
349 if untracked not in self.get_untracked():
350 self.add_untracked(untracked)
352 self.set_branch(git.current_branch())
353 self.set_unstaged(self.get_modified() + self.get_untracked())
354 self.set_remotes(git.remote().splitlines())
355 self.set_remote_branches(git.branch(remote=True))
356 self.set_local_branches(git.branch(remote=False))
357 self.set_tags(git.tag().splitlines())
358 self.set_revision('')
359 self.set_local_branch('')
360 self.set_remote_branch('')
361 # Re-enable notifications and emit changes
362 self.set_notify(notify_enabled)
363 self.notify_observers('staged','unstaged')
365 def delete_branch(self, branch):
366 return git.branch(name=branch, delete=True)
368 def get_revision_sha1(self, idx):
369 return self.get_revisions()[idx]
371 def get_config_params(self):
372 params = []
373 params.extend(map(lambda x: 'local_' + x,
374 self.__local_and_global_defaults.keys()))
375 params.extend(map(lambda x: 'global_' + x,
376 self.__local_and_global_defaults.keys()))
377 params.extend(map(lambda x: 'global_' + x,
378 self.__global_defaults.keys()))
379 return params
381 def apply_font_size(self, param, default):
382 old_font = self.get_param(param)
383 if not old_font:
384 old_font = default
386 size = self.get_param(param+'_size')
387 props = old_font.split(',')
388 props[1] = str(size)
389 new_font = ','.join(props)
391 self.set_param(param, new_font)
393 def read_font_size(self, param, new_font):
394 new_size = int(new_font.split(',')[1])
395 self.set_param(param, new_size)
397 def get_commit_diff(self, sha1):
398 commit = git.show(sha1)
399 first_newline = commit.index('\n')
400 if commit[first_newline+1:].startswith('Merge:'):
401 return (commit
402 + '\n\n'
403 + self.diff_helper(
404 commit=sha1,
405 cached=False,
406 suppress_header=False,
409 else:
410 return commit
412 def get_diff_and_status(self, idx, staged=True):
413 if staged:
414 filename = self.get_staged()[idx]
415 if os.path.exists(filename):
416 status = 'Staged for commit'
417 else:
418 status = 'Staged for removal'
419 diff = self.diff_helper(
420 filename=filename,
421 cached=True,
423 else:
424 filename = self.get_unstaged()[idx]
425 if os.path.isdir(filename):
426 status = 'Untracked directory'
427 diff = '\n'.join(os.listdir(filename))
428 elif filename in self.get_modified():
429 status = 'Modified, not staged'
430 diff = self.diff_helper(
431 filename=filename,
432 cached=False,
434 else:
435 status = 'Untracked, not staged'
437 file_type = utils.run_cmd('file',filename, b=True)
438 if 'binary' in file_type or 'data' in file_type:
439 diff = utils.run_cmd('hexdump', filename, C=True)
440 else:
441 if os.path.exists(filename):
442 file = open(filename, 'r')
443 diff = file.read()
444 file.close()
445 else:
446 diff = ''
447 return diff, status
449 def stage_modified(self):
450 output = git.add(self.get_modified())
451 self.update_status()
452 return output
454 def stage_untracked(self):
455 output = git.add(self.get_untracked())
456 self.update_status()
457 return output
459 def reset(self, *items):
460 output = git.reset('--', *items)
461 self.update_status()
462 return output
464 def unstage_all(self):
465 git.reset('--', *self.get_staged())
466 self.update_status()
468 def save_gui_settings(self):
469 git.config_set('ugit.geometry', utils.get_geom(), local=False)