cmds: teach checkout and remote about the dispatcher
[ugit.git] / ugit / models.py
blobc11065ea810ac276cdce7b058326f1d05ebe289a
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 'commit_with_msg',
37 'diff',
38 'diff_helper',
39 'diffstat',
40 'diffindex',
41 'format_patch',
42 'push',
43 'show',
44 'log',
45 'rebase',
46 'remote_url',
47 'rev_list_range',
49 setattr(self, cmd, getattr(git,cmd))
51 self.create(
52 #####################################################
53 # Used in various places
54 branch = '',
55 remotes = [],
56 remote = '',
57 local_branch = '',
58 remote_branch = '',
59 search_text = '',
60 git_version = git.version(),
62 #####################################################
63 # Used primarily by the main UI
64 project = os.path.basename(os.getcwd()),
65 commitmsg = '',
66 modified = [],
67 staged = [],
68 unstaged = [],
69 untracked = [],
70 window_geom = utils.parse_geom(
71 self.get_global_ugit_geometry()),
73 #####################################################
74 # Used by the create branch dialog
75 revision = '',
76 local_branches = [],
77 remote_branches = [],
78 tags = [],
80 #####################################################
81 # Used by the commit/repo browser
82 directory = '',
83 revisions = [],
84 summaries = [],
86 # These are parallel lists
87 types = [],
88 sha1s = [],
89 names = [],
91 # All items below here are re-calculated in
92 # init_browser_data()
93 directories = [],
94 directory_entries = {},
96 # These are also parallel lists
97 subtree_types = [],
98 subtree_sha1s = [],
99 subtree_names = [],
103 def init_config_data(self):
104 """Reads git config --list and creates parameters
105 for each setting."""
106 # These parameters are saved in .gitconfig,
107 # so ideally these should be as short as possible.
109 # config items that are controllable globally
110 # and per-repository
111 self.__local_and_global_defaults = {
112 'user_name': '',
113 'user_email': '',
114 'merge_summary': False,
115 'merge_diffstat': True,
116 'merge_verbosity': 2,
117 'gui_diffcontext': 5,
118 'gui_pruneduringfetch': False,
120 # config items that are purely git config --global settings
121 self.__global_defaults = {
122 'ugit_geometry':'',
123 'ugit_fontui': '',
124 'ugit_fontui_size':12,
125 'ugit_fontdiff': '',
126 'ugit_fontdiff_size':12,
127 'ugit_historybrowser': 'gitk',
128 'ugit_savewindowsettings': False,
129 'ugit_saveatexit': False,
132 local_dict = git.config_dict(local=True)
133 global_dict = git.config_dict(local=False)
135 for k,v in local_dict.iteritems():
136 self.set_param('local_'+k, v)
137 for k,v in global_dict.iteritems():
138 self.set_param('global_'+k, v)
139 if k not in local_dict:
140 local_dict[k]=v
141 self.set_param('local_'+k, v)
143 # Bootstrap the internal font*_size variables
144 for param in ('global_ugit_fontui', 'global_ugit_fontdiff'):
145 if hasattr(self, param):
146 font = self.get_param(param)
147 if font:
148 size = int(font.split(',')[1])
149 self.set_param(param+'_size', size)
150 param = param[len('global_'):]
151 global_dict[param] = font
152 global_dict[param+'_size'] = size
154 # Load defaults for all undefined items
155 local_and_global_defaults = self.__local_and_global_defaults
156 for k,v in local_and_global_defaults.iteritems():
157 if k not in local_dict:
158 self.set_param('local_'+k, v)
159 if k not in global_dict:
160 self.set_param('global_'+k, v)
162 global_defaults = self.__global_defaults
163 for k,v in global_defaults.iteritems():
164 if k not in global_dict:
165 self.set_param('global_'+k, v)
167 def save_config_param(self, param):
168 if param not in self.get_config_params():
169 return
170 value = self.get_param(param)
171 if param == 'local_gui_diffcontext':
172 git.DIFF_CONTEXT = value
173 if param.startswith('local_'):
174 param = param[len('local_'):]
175 is_local = True
176 elif param.startswith('global_'):
177 param = param[len('global_'):]
178 is_local = False
179 else:
180 raise Exception("Invalid param '%s' passed to " % param
181 + "save_config_param()")
182 param = param.replace('_','.') # model -> git
183 return git.config_set(param, value, local=is_local)
185 def init_browser_data(self):
186 '''This scans over self.(names, sha1s, types) to generate
187 directories, directory_entries, and subtree_*'''
189 # Collect data for the model
190 if not self.get_branch(): return
192 self.subtree_types = []
193 self.subtree_sha1s = []
194 self.subtree_names = []
195 self.directories = []
196 self.directory_entries = {}
198 # Lookup the tree info
199 tree_info = git.ls_tree(self.get_branch())
201 self.set_types(map( lambda(x): x[1], tree_info ))
202 self.set_sha1s(map( lambda(x): x[2], tree_info ))
203 self.set_names(map( lambda(x): x[3], tree_info ))
205 if self.directory: self.directories.append('..')
207 dir_entries = self.directory_entries
208 dir_regex = re.compile('([^/]+)/')
209 dirs_seen = {}
210 subdirs_seen = {}
212 for idx, name in enumerate(self.names):
214 if not name.startswith(self.directory): continue
215 name = name[ len(self.directory): ]
217 if name.count('/'):
218 # This is a directory...
219 match = dir_regex.match(name)
220 if not match: continue
222 dirent = match.group(1) + '/'
223 if dirent not in self.directory_entries:
224 self.directory_entries[dirent] = []
226 if dirent not in dirs_seen:
227 dirs_seen[dirent] = True
228 self.directories.append(dirent)
230 entry = name.replace(dirent, '')
231 entry_match = dir_regex.match(entry)
232 if entry_match:
233 subdir = entry_match.group(1) + '/'
234 if subdir in subdirs_seen: continue
235 subdirs_seen[subdir] = True
236 dir_entries[dirent].append(subdir)
237 else:
238 dir_entries[dirent].append(entry)
239 else:
240 self.subtree_types.append(self.types[idx])
241 self.subtree_sha1s.append(self.sha1s[idx])
242 self.subtree_names.append(name)
244 def get_history_browser(self):
245 return self.get_param('global_ugit_historybrowser')
247 def remember_gui_settings(self):
248 return self.get_param('global_ugit_savewindowsettings')
250 def save_at_exit(self):
251 return self.get_param('global_ugit_saveatexit')
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 =('\n\nSigned-off-by: %s <%s>\n' % (
279 self.get_local_user_name(),
280 self.get_local_user_email()))
282 if signoff not in msg:
283 self.set_commitmsg(msg + signoff)
285 def apply_diff(self, filename):
286 return git.apply(filename, index=True, cached=True)
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('\n'.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 modified and untracked files.
324 self.staged = []
325 self.modified = []
326 self.untracked = []
328 # Read git status items
329 ( staged_items,
330 modified_items,
331 untracked_items ) = git.parsed_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 modified in modified_items:
340 if modified not in self.get_modified():
341 self.add_modified(modified)
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_unstaged(self.get_modified() + self.get_untracked())
350 self.set_remotes(git.remote().splitlines())
351 self.set_remote_branches(git.branch(remote=True))
352 self.set_local_branches(git.branch(remote=False))
353 self.set_tags(git.tag().splitlines())
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('staged','unstaged')
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 get_config_params(self):
368 params = []
369 params.extend(map(lambda x: 'local_' + x,
370 self.__local_and_global_defaults.keys()))
371 params.extend(map(lambda x: 'global_' + x,
372 self.__local_and_global_defaults.keys()))
373 params.extend(map(lambda x: 'global_' + x,
374 self.__global_defaults.keys()))
375 return params
377 def apply_font_size(self, param, default):
378 old_font = self.get_param(param)
379 if not old_font:
380 old_font = default
382 size = self.get_param(param+'_size')
383 props = old_font.split(',')
384 props[1] = str(size)
385 new_font = ','.join(props)
387 self.set_param(param, new_font)
389 def read_font_size(self, param, new_font):
390 new_size = int(new_font.split(',')[1])
391 self.set_param(param, new_size)
393 def get_commit_diff(self, sha1):
394 commit = git.show(sha1)
395 first_newline = commit.index('\n')
396 if commit[first_newline+1:].startswith('Merge:'):
397 return (commit
398 + '\n\n'
399 + self.diff_helper(
400 commit=sha1,
401 cached=False,
402 suppress_header=False,
405 else:
406 return commit
408 def get_diff_and_status(self, idx, staged=True):
409 if staged:
410 filename = self.get_staged()[idx]
411 if os.path.exists(filename):
412 status = 'Staged for commit'
413 else:
414 status = 'Staged for removal'
415 diff = self.diff_helper(
416 filename=filename,
417 cached=True,
419 else:
420 filename = self.get_unstaged()[idx]
421 if os.path.isdir(filename):
422 status = 'Untracked directory'
423 diff = '\n'.join(os.listdir(filename))
424 elif filename in self.get_modified():
425 status = 'Modified, not staged'
426 diff = self.diff_helper(
427 filename=filename,
428 cached=False,
430 else:
431 status = 'Untracked, not staged'
433 file_type = utils.run_cmd('file',filename, b=True)
434 if 'binary' in file_type or 'data' in file_type:
435 diff = utils.run_cmd('hexdump', filename, C=True)
436 else:
437 if os.path.exists(filename):
438 file = open(filename, 'r')
439 diff = file.read()
440 file.close()
441 else:
442 diff = ''
443 return diff, status
445 def stage_modified(self):
446 output = git.add(self.get_modified())
447 self.update_status()
448 return output
450 def stage_untracked(self):
451 output = git.add(self.get_untracked())
452 self.update_status()
453 return output
455 def reset(self, *items):
456 output = git.reset('--', *items)
457 self.update_status()
458 return output
460 def unstage_all(self):
461 git.reset('--', *self.get_staged())
462 self.update_status()
464 def save_gui_settings(self):
465 git.config_set('ugit.geometry', utils.get_geom(), local=False)