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