utils: remove QProcess usage
[ugit.git] / ugit / models.py
blobdd2475e64fc14b54dcea5d87eeec2a0a609c4024
1 import os
2 import sys
3 import re
5 from ugit import git
6 from ugit import utils
7 from ugit import model
9 class Model(model.Model):
10 """Provides a friendly wrapper for doing commit git operations."""
12 def init(self):
13 """Reads git repository settings and sets severl methods
14 so that they refer to the git module. This object is
15 encapsulates ugit's interaction with git.
16 The git module itself should know nothing about ugit
17 whatsoever."""
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 if cdup.startswith('fatal:'):
24 # this is not a git repo
25 sys.stderr.write(cdup+"\n")
26 sys.exit(-1)
27 os.chdir(cdup)
29 # Read git config
30 self.init_config_data()
32 # Import all git commands from git.py
33 for name, cmd in git.commands.iteritems():
34 setattr(self, name, cmd)
36 self.create(
37 #####################################################
38 # Used in various places
39 currentbranch = '',
40 remotes = [],
41 remote = '',
42 local_branch = '',
43 remote_branch = '',
44 search_text = '',
45 git_version = git.version(),
47 #####################################################
48 # Used primarily by the main UI
49 project = os.path.basename(os.getcwd()),
50 commitmsg = '',
51 modified = [],
52 staged = [],
53 unstaged = [],
54 untracked = [],
55 window_geom = utils.parse_geom(
56 self.get_global_ugit_geometry()),
58 #####################################################
59 # Used by the create branch dialog
60 revision = '',
61 local_branches = [],
62 remote_branches = [],
63 tags = [],
65 #####################################################
66 # Used by the commit/repo browser
67 directory = '',
68 revisions = [],
69 summaries = [],
71 # These are parallel lists
72 types = [],
73 sha1s = [],
74 names = [],
76 # All items below here are re-calculated in
77 # init_browser_data()
78 directories = [],
79 directory_entries = {},
81 # These are also parallel lists
82 subtree_types = [],
83 subtree_sha1s = [],
84 subtree_names = [],
88 def init_config_data(self):
89 """Reads git config --list and creates parameters
90 for each setting."""
91 # These parameters are saved in .gitconfig,
92 # so ideally these should be as short as possible.
94 # config items that are controllable globally
95 # and per-repository
96 self.__local_and_global_defaults = {
97 'user_name': '',
98 'user_email': '',
99 'merge_summary': False,
100 'merge_diffstat': True,
101 'merge_verbosity': 2,
102 'gui_diffcontext': 5,
103 'gui_pruneduringfetch': False,
105 # config items that are purely git config --global settings
106 self.__global_defaults = {
107 'ugit_geometry':'',
108 'ugit_fontui': '',
109 'ugit_fontui_size':12,
110 'ugit_fontdiff': '',
111 'ugit_fontdiff_size':12,
112 'ugit_historybrowser': 'gitk',
113 'ugit_savewindowsettings': False,
114 'ugit_saveatexit': False,
117 local_dict = git.config_dict(local=True)
118 global_dict = git.config_dict(local=False)
120 for k,v in local_dict.iteritems():
121 self.set_param('local_'+k, v)
122 for k,v in global_dict.iteritems():
123 self.set_param('global_'+k, v)
124 if k not in local_dict:
125 local_dict[k]=v
126 self.set_param('local_'+k, v)
128 # Bootstrap the internal font*_size variables
129 for param in ('global_ugit_fontui', 'global_ugit_fontdiff'):
130 if hasattr(self, param):
131 font = self.get_param(param)
132 if font:
133 size = int(font.split(',')[1])
134 self.set_param(param+'_size', size)
135 param = param[len('global_'):]
136 global_dict[param] = font
137 global_dict[param+'_size'] = size
139 # Load defaults for all undefined items
140 local_and_global_defaults = self.__local_and_global_defaults
141 for k,v in local_and_global_defaults.iteritems():
142 if k not in local_dict:
143 self.set_param('local_'+k, v)
144 if k not in global_dict:
145 self.set_param('global_'+k, v)
147 global_defaults = self.__global_defaults
148 for k,v in global_defaults.iteritems():
149 if k not in global_dict:
150 self.set_param('global_'+k, v)
152 def save_config_param(self, param):
153 if param not in self.get_config_params():
154 return
155 value = self.get_param(param)
156 if param == 'local_gui_diffcontext':
157 git.DIFF_CONTEXT = value
158 if param.startswith('local_'):
159 param = param[len('local_'):]
160 is_local = True
161 elif param.startswith('global_'):
162 param = param[len('global_'):]
163 is_local = False
164 else:
165 raise Exception("Invalid param '%s' passed to " % param
166 + "save_config_param()")
167 param = param.replace('_','.') # model -> git
168 return git.config_set(param, value, local=is_local)
170 def init_browser_data(self):
171 '''This scans over self.(names, sha1s, types) to generate
172 directories, directory_entries, and subtree_*'''
174 # Collect data for the model
175 if not self.get_currentbranch(): return
177 self.subtree_types = []
178 self.subtree_sha1s = []
179 self.subtree_names = []
180 self.directories = []
181 self.directory_entries = {}
183 # Lookup the tree info
184 tree_info = git.parse_ls_tree(self.get_currentbranch())
186 self.set_types(map( lambda(x): x[1], tree_info ))
187 self.set_sha1s(map( lambda(x): x[2], tree_info ))
188 self.set_names(map( lambda(x): x[3], tree_info ))
190 if self.directory: self.directories.append('..')
192 dir_entries = self.directory_entries
193 dir_regex = re.compile('([^/]+)/')
194 dirs_seen = {}
195 subdirs_seen = {}
197 for idx, name in enumerate(self.names):
199 if not name.startswith(self.directory): continue
200 name = name[ len(self.directory): ]
202 if name.count('/'):
203 # This is a directory...
204 match = dir_regex.match(name)
205 if not match: continue
207 dirent = match.group(1) + '/'
208 if dirent not in self.directory_entries:
209 self.directory_entries[dirent] = []
211 if dirent not in dirs_seen:
212 dirs_seen[dirent] = True
213 self.directories.append(dirent)
215 entry = name.replace(dirent, '')
216 entry_match = dir_regex.match(entry)
217 if entry_match:
218 subdir = entry_match.group(1) + '/'
219 if subdir in subdirs_seen: continue
220 subdirs_seen[subdir] = True
221 dir_entries[dirent].append(subdir)
222 else:
223 dir_entries[dirent].append(entry)
224 else:
225 self.subtree_types.append(self.types[idx])
226 self.subtree_sha1s.append(self.sha1s[idx])
227 self.subtree_names.append(name)
229 def get_history_browser(self):
230 return self.get_param('global_ugit_historybrowser')
232 def remember_gui_settings(self):
233 return self.get_param('global_ugit_savewindowsettings')
235 def save_at_exit(self):
236 return self.get_param('global_ugit_saveatexit')
238 def get_tree_node(self, idx):
239 return (self.get_types()[idx],
240 self.get_sha1s()[idx],
241 self.get_names()[idx] )
243 def get_subtree_node(self, idx):
244 return (self.get_subtree_types()[idx],
245 self.get_subtree_sha1s()[idx],
246 self.get_subtree_names()[idx] )
248 def get_all_branches(self):
249 return (self.get_local_branches() + self.get_remote_branches())
251 def set_remote(self, remote):
252 if not remote: return
253 self.set_param('remote', remote)
254 branches = utils.grep( '%s/\S+$' % remote,
255 git.branch_list(remote=True), squash=False)
256 self.set_remote_branches(branches)
258 def add_signoff(self,*rest):
259 '''Adds a standard Signed-off by: tag to the end
260 of the current commit message.'''
262 msg = self.get_commitmsg()
263 signoff =('\n\nSigned-off-by: %s <%s>\n' % (
264 self.get_local_user_name(),
265 self.get_local_user_email()))
267 if signoff not in msg:
268 self.set_commitmsg(msg + signoff)
270 def apply_diff(self, filename):
271 return git.apply(filename, index=True, cached=True)
273 def load_commitmsg(self, path):
274 file = open(path, 'r')
275 contents = file.read()
276 file.close()
277 self.set_commitmsg(contents)
279 def get_prev_commitmsg(self,*rest):
280 '''Queries git for the latest commit message and sets it in
281 self.commitmsg.'''
282 commit_msg = []
283 commit_lines = git.show('HEAD').split('\n')
284 for idx, msg in enumerate(commit_lines):
285 if idx < 4: continue
286 msg = msg.lstrip()
287 if msg.startswith('diff --git'):
288 commit_msg.pop()
289 break
290 commit_msg.append(msg)
291 self.set_commitmsg('\n'.join(commit_msg).rstrip())
293 def update_status(self):
294 # This allows us to defer notification until the
295 # we finish processing data
296 notify_enabled = self.get_notify()
297 self.set_notify(False)
299 # Reset the staged and unstaged model lists
300 # NOTE: the model's unstaged list is used to
301 # hold both modified and untracked files.
302 self.staged = []
303 self.modified = []
304 self.untracked = []
306 # Read git status items
307 ( staged_items,
308 modified_items,
309 untracked_items ) = git.parse_status()
311 # Gather items to be committed
312 for staged in staged_items:
313 if staged not in self.get_staged():
314 self.add_staged(staged)
316 # Gather unindexed items
317 for modified in modified_items:
318 if modified not in self.get_modified():
319 self.add_modified(modified)
321 # Gather untracked items
322 for untracked in untracked_items:
323 if untracked not in self.get_untracked():
324 self.add_untracked(untracked)
326 self.set_currentbranch(git.current_branch())
327 self.set_unstaged(self.get_modified() + self.get_untracked())
328 self.set_remotes(git.remote().splitlines())
329 self.set_remote_branches(git.branch_list(remote=True))
330 self.set_local_branches(git.branch_list(remote=False))
331 self.set_tags(git.tag().splitlines())
332 self.set_revision('')
333 self.set_local_branch('')
334 self.set_remote_branch('')
335 # Re-enable notifications and emit changes
336 self.set_notify(notify_enabled)
337 self.notify_observers('staged','unstaged')
339 def delete_branch(self, branch):
340 return git.branch(branch, D=True)
342 def get_revision_sha1(self, idx):
343 return self.get_revisions()[idx]
345 def get_config_params(self):
346 params = []
347 params.extend(map(lambda x: 'local_' + x,
348 self.__local_and_global_defaults.keys()))
349 params.extend(map(lambda x: 'global_' + x,
350 self.__local_and_global_defaults.keys()))
351 params.extend(map(lambda x: 'global_' + x,
352 self.__global_defaults.keys()))
353 return params
355 def apply_font_size(self, param, default):
356 old_font = self.get_param(param)
357 if not old_font:
358 old_font = default
360 size = self.get_param(param+'_size')
361 props = old_font.split(',')
362 props[1] = str(size)
363 new_font = ','.join(props)
365 self.set_param(param, new_font)
367 def read_font_size(self, param, new_font):
368 new_size = int(new_font.split(',')[1])
369 self.set_param(param, new_size)
371 def get_commit_diff(self, sha1):
372 commit = git.show(sha1)
373 first_newline = commit.index('\n')
374 if commit[first_newline+1:].startswith('Merge:'):
375 return (commit
376 + '\n\n'
377 + self.diff_helper(
378 commit=sha1,
379 cached=False,
380 suppress_header=False,
383 else:
384 return commit
386 def get_diff_and_status(self, idx, staged=True):
387 if staged:
388 filename = self.get_staged()[idx]
389 if os.path.exists(filename):
390 status = 'Staged for commit'
391 else:
392 status = 'Staged for removal'
393 diff = self.diff_helper(
394 filename=filename,
395 cached=True,
397 else:
398 filename = self.get_unstaged()[idx]
399 if os.path.isdir(filename):
400 status = 'Untracked directory'
401 diff = '\n'.join(os.listdir(filename))
402 elif filename in self.get_modified():
403 status = 'Modified, not staged'
404 diff = self.diff_helper(
405 filename=filename,
406 cached=False,
408 else:
409 status = 'Untracked, not staged'
411 file_type = utils.run_cmd('file',filename, b=True)
412 if 'binary' in file_type or 'data' in file_type:
413 diff = utils.run_cmd('hexdump', filename, C=True)
414 else:
415 if os.path.exists(filename):
416 file = open(filename, 'r')
417 diff = file.read()
418 file.close()
419 else:
420 diff = ''
421 return diff, status
423 def stage_modified(self):
424 output = git.add(self.get_modified())
425 self.update_status()
426 return output
428 def stage_untracked(self):
429 output = git.add(self.get_untracked())
430 self.update_status()
431 return output
433 def reset(self, *items):
434 output = git.reset('--', *items)
435 self.update_status()
436 return output
438 def unstage_all(self):
439 git.reset('--', *self.get_staged())
440 self.update_status()
442 def save_gui_settings(self):
443 git.config_set('ugit.geometry', utils.get_geom(), local=False)