Added option edit/save support via git config.
[ugit.git] / ugitlibs / models.py
blob2551e3f9cd1fad5e4ccd615152794794095a0212
1 import os
2 import re
4 import git
5 import utils
6 import model
8 class Model(model.Model):
9 def __init__(self):
10 model.Model.__init__(self)
12 # These methods are best left implemented in git.py
13 git_attrs=(
14 'add',
15 'add_or_remove',
16 'cat_file',
17 'checkout',
18 'create_branch',
19 'cherry_pick',
20 'commit',
21 'diff',
22 'diff_stat',
23 'format_patch',
24 'push',
25 'show',
26 'log',
27 'rebase',
28 'remote_url',
29 'rev_list_range',
32 for attr in git_attrs:
33 setattr(self, attr, getattr(git,attr))
35 # chdir to the root of the git tree. This is critical
36 # to being able to properly use the git porcelain.
37 cdup = git.show_cdup()
38 if cdup: os.chdir(cdup)
39 self.init_config_data()
40 self.create(
41 #####################################################
42 # Used in various places
43 branch = git.current_branch(),
44 remotes = git.remote(),
45 remote = '',
46 local_branch = '',
47 remote_branch = '',
49 #####################################################
50 # Used primarily by the main UI
51 window_geom = utils.parse_geom(
52 self.get_param('global.ugit.geometry')),
53 project = os.path.basename(os.getcwd()),
54 commitmsg = '',
55 staged = [],
56 unstaged = [],
57 untracked = [],
58 all_unstaged = [], # unstaged+untracked
60 #####################################################
61 # Used by the create branch dialog
62 revision = '',
63 local_branches = git.branch(remote=False),
64 remote_branches = git.branch(remote=True),
65 tags = git.tag(),
67 #####################################################
68 # Used by the commit/repo browser
69 directory = '',
70 revisions = [],
71 summaries = [],
73 # These are parallel lists
74 types = [],
75 sha1s = [],
76 names = [],
78 # All items below here are re-calculated in
79 # init_browser_data()
80 directories = [],
81 directory_entries = {},
83 # These are also parallel lists
84 subtree_types = [],
85 subtree_sha1s = [],
86 subtree_names = [],
89 def init_config_data(self):
90 self.__allowed_params = [
91 'user.name',
92 'user.email',
93 'merge.summary',
94 'merge.diffstat',
95 'merge.verbosity',
96 'gui.diffcontext',
97 'gui.pruneduringfetch',
98 'ugit.geometry',
99 'ugit.fontui',
100 'ugit.fontdiff',
102 self.__config_types = {}
103 self.__config_defaults = {
104 'user.name': '',
105 'user.email': '',
106 'merge.summary': False,
107 'merge.diffstat': True,
108 'merge.verbosity': 2,
109 'gui.diffcontext': 5,
110 'gui.pruneduringfetch': False,
112 self.__global_defaults = {
113 'ugit.geometry':'',
114 'ugit.fontui': '',
115 'ugit.fontui.size':12,
116 'ugit.fontdiff': '',
117 'ugit.fontdiff.size':12,
120 default_dict = self.__config_defaults
121 if self.__config_types: return
122 for k,v in default_dict.iteritems():
123 if type(v) is int:
124 self.__config_types[k] = 'int'
125 elif type(v) is bool:
126 self.__config_types[k] = 'bool'
128 def config_to_dict(config):
129 newdict = {}
130 for line in config.splitlines():
131 k, v = line.split('=')
132 try:
133 linetype = self.__config_types[k]
134 if linetype == 'int':
135 v = int(v)
136 elif linetype == 'bool':
137 v = bool(eval(v.title()))
138 except: pass
139 newdict[k]=v
140 return newdict
142 local_conf = git.git('config', '--list')
143 global_conf = git.git('config', '--global', '--list')
144 local_dict = config_to_dict(local_conf)
145 global_dict = config_to_dict(global_conf)
147 for k,v in local_dict.iteritems():
148 self.set_param('local.'+k, v)
149 for k,v in global_dict.iteritems():
150 self.set_param('global.'+k, v)
151 if k not in local_dict:
152 local_dict[k]=v
153 self.set_param('local.'+k, v)
155 # internal bootstrap variables
156 for param in ('global.ugit.fontui',
157 'global.ugit.fontdiff'):
158 if hasattr(self, param):
159 font = self.get_param(param)
160 if font:
161 size = int(font.split(',')[1])
162 self.set_param(param+'.size', size)
163 param = param[len('global.'):]
164 global_dict[param] = font
165 global_dict[param+'.size'] = size
167 # Load defaults for all undefined items
168 for k,v in default_dict.iteritems():
169 if k not in local_dict:
170 self.set_param('local.'+k, v)
171 if k not in global_dict:
172 self.set_param('global.'+k, v)
174 for k,v in self.__global_defaults.iteritems():
175 if k not in global_dict:
176 self.set_param('global.'+k, v)
178 def save_config_param(self,param):
179 value = self.get_param(param)
180 old_param = param
181 if param.startswith('local.'):
182 param = param[len('local.'):]
183 is_local = True
184 elif param.startswith('global.'):
185 param = param[len('global.'):]
186 is_local = False
187 if param not in self.__allowed_params:
188 return
189 git.config(param, value, local=is_local)
190 if old_param == 'local.gui.diffcontext':
191 git.DIFF_CONTEXT = \
192 self.get_param('local.gui.diffcontext')
194 def init_browser_data(self):
195 '''This scans over self.(names, sha1s, types) to generate
196 directories, directory_entries, and subtree_*'''
198 # Collect data for the model
199 if not self.get_branch(): return
201 self.subtree_types = []
202 self.subtree_sha1s = []
203 self.subtree_names = []
204 self.directories = []
205 self.directory_entries = {}
207 # Lookup the tree info
208 tree_info = git.ls_tree(self.get_branch())
210 self.set_types(map( lambda(x): x[1], tree_info ))
211 self.set_sha1s(map( lambda(x): x[2], tree_info ))
212 self.set_names(map( lambda(x): x[3], tree_info ))
214 if self.directory: self.directories.append('..')
216 dir_entries = self.directory_entries
217 dir_regex = re.compile('([^/]+)/')
218 dirs_seen = {}
219 subdirs_seen = {}
221 for idx, name in enumerate(self.names):
223 if not name.startswith(self.directory): continue
224 name = name[ len(self.directory): ]
226 if name.count('/'):
227 # This is a directory...
228 match = dir_regex.match(name)
229 if not match: continue
231 dirent = match.group(1) + '/'
232 if dirent not in self.directory_entries:
233 self.directory_entries[dirent] = []
235 if dirent not in dirs_seen:
236 dirs_seen[dirent] = True
237 self.directories.append(dirent)
239 entry = name.replace(dirent, '')
240 entry_match = dir_regex.match(entry)
241 if entry_match:
242 subdir = entry_match.group(1) + '/'
243 if subdir in subdirs_seen: continue
244 subdirs_seen[subdir] = True
245 dir_entries[dirent].append(subdir)
246 else:
247 dir_entries[dirent].append(entry)
248 else:
249 self.subtree_types.append(self.types[idx])
250 self.subtree_sha1s.append(self.sha1s[idx])
251 self.subtree_names.append(name)
253 def get_tree_node(self, idx):
254 return (self.get_types()[idx],
255 self.get_sha1s()[idx],
256 self.get_names()[idx] )
258 def get_subtree_node(self, idx):
259 return (self.get_subtree_types()[idx],
260 self.get_subtree_sha1s()[idx],
261 self.get_subtree_names()[idx] )
263 def get_all_branches(self):
264 return (self.get_local_branches() + self.get_remote_branches())
266 def set_remote(self,remote):
267 if not remote: return
268 self.set_param('remote',remote)
269 branches = utils.grep( '%s/\S+$' % remote,
270 git.branch(remote=True), squash=False)
271 self.set_remote_branches(branches)
273 def add_signoff(self,*rest):
274 '''Adds a standard Signed-off by: tag to the end
275 of the current commit message.'''
277 msg = self.get_commitmsg()
278 signoff =('Signed-off by: %s <%s>' % (
279 self.get_param('local.user.name'),
280 self.get_param('local.user.email')))
282 if signoff not in msg:
283 self.set_commitmsg(msg + os.linesep*2 + signoff)
285 def apply_diff(self, filename):
286 return git.apply(filename)
288 def __get_squash_msg_path(self):
289 return os.path.join(os.getcwd(), '.git', 'SQUASH_MSG')
291 def has_squash_msg(self):
292 squash_msg = self.__get_squash_msg_path()
293 return os.path.exists(squash_msg)
295 def get_squash_msg(self):
296 return utils.slurp(self.__get_squash_msg_path())
298 def set_squash_msg(self):
299 self.set_commitmsg(self.get_squash_msg())
301 def get_prev_commitmsg(self,*rest):
302 '''Queries git for the latest commit message and sets it in
303 self.commitmsg.'''
304 commit_msg = []
305 commit_lines = git.show('HEAD').split('\n')
306 for idx, msg in enumerate(commit_lines):
307 if idx < 4: continue
308 msg = msg.lstrip()
309 if msg.startswith('diff --git'):
310 commit_msg.pop()
311 break
312 commit_msg.append(msg)
313 self.set_commitmsg(os.linesep.join(commit_msg).rstrip())
315 def update_status(self):
316 # This allows us to defer notification until the
317 # we finish processing data
318 notify_enabled = self.get_notify()
319 self.set_notify(False)
321 # Reset the staged and unstaged model lists
322 # NOTE: the model's unstaged list is used to
323 # hold both unstaged and untracked files.
324 self.staged = []
325 self.unstaged = []
326 self.untracked = []
328 # Read git status items
329 ( staged_items,
330 unstaged_items,
331 untracked_items ) = git.status()
333 # Gather items to be committed
334 for staged in staged_items:
335 if staged not in self.get_staged():
336 self.add_staged(staged)
338 # Gather unindexed items
339 for unstaged in unstaged_items:
340 if unstaged not in self.get_unstaged():
341 self.add_unstaged(unstaged)
343 # Gather untracked items
344 for untracked in untracked_items:
345 if untracked not in self.get_untracked():
346 self.add_untracked(untracked)
348 self.set_branch(git.current_branch())
349 self.set_all_unstaged(self.get_unstaged() + self.get_untracked())
350 self.set_remotes(git.remote())
351 self.set_remote_branches(git.branch(remote=True))
352 self.set_local_branches(git.branch(remote=False))
353 self.set_tags(git.tag())
354 self.set_revision('')
355 self.set_local_branch('')
356 self.set_remote_branch('')
357 # Re-enable notifications and emit changes
358 self.set_notify(notify_enabled)
359 self.notify_observers('all_unstaged', 'staged')
361 def delete_branch(self, branch):
362 return git.branch(name=branch, delete=True)
364 def get_revision_sha1(self, idx):
365 return self.get_revisions()[idx]
367 def set_local_config(self, param, val):
368 self.set_param(self, 'local.'+param, val)
370 def set_global_config(self, param, val):
371 self.set_param(self, 'global.'+param, val)
373 def get_config_params(self):
374 params = []
375 params.extend(map(lambda x: 'local.' + x,
376 self.__config_defaults.keys()))
377 params.extend(map(lambda x: 'global.' + x,
378 self.__config_defaults.keys()))
379 params.extend(map(lambda x: 'global.' + x,
380 self.__global_defaults.keys()))
381 return params
383 def apply_font_size(self, param, default):
384 old_font = self.get_param(param)
385 if not old_font:
386 old_font = default
388 size = self.get_param(param+'.size')
389 props = old_font.split(',')
390 props[1] = str(size)
391 new_font = ','.join(props)
393 self.set_param(param, new_font)
395 def read_font_size(self, param, new_font):
396 new_size = int(new_font.split(',')[1])
397 self.set_param(param, new_size)
399 def get_commit_diff(self, sha1):
400 commit = self.show(sha1)
401 first_newline = commit.index(os.linesep)
402 merge = commit[first_newline+1:].startswith('Merge:')
403 if merge:
404 return (commit + os.linesep*2
405 + self.diff(commit=sha1, cached=False,
406 suppress_header=False))
407 else:
408 return commit
410 def get_unstaged_item(self, idx):
411 return self.get_all_unstaged()[idx]
413 def get_diff_and_status(self, idx, staged=True):
414 if staged:
415 filename = self.get_staged()[idx]
416 if os.path.exists(filename):
417 status = 'Staged for commit'
418 else:
419 status = 'Staged for removal'
420 diff = self.diff(filename=filename, cached=True)
421 else:
422 filename = self.get_all_unstaged()[idx]
423 if os.path.isdir(filename):
424 status = 'Untracked directory'
425 diff = os.linesep.join(os.listdir(filename))
426 elif filename in self.get_unstaged():
427 status = 'Modified, not staged'
428 diff = self.diff(filename=filename, cached=False)
429 else:
430 status = 'Untracked, not staged'
432 file_type = utils.run_cmd('file','-b',filename)
433 if 'binary' in file_type or 'data' in file_type:
434 diff = utils.run_cmd('hexdump','-C',filename)
435 else:
436 if os.path.exists(filename):
437 file = open(filename, 'r')
438 diff = file.read()
439 file.close()
440 else:
441 diff = ''
442 return diff, status
444 def stage_changed(self):
445 git.add(self.get_unstaged())
446 self.update_status()
448 def stage_untracked(self):
449 git.add(self.get_untracked())
450 self.update_status()
452 def reset(self, items):
453 git.reset(items)
454 self.update_status()
456 def unstage_all(self):
457 git.reset(self.get_staged())
458 self.update_status()
460 def save_window_geom(self):
461 git.config('ugit.geometry', utils.get_geom(), local=False)