8 class Model(model
.Model
):
9 """Provides a friendly wrapper for doing commit git operations."""
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
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)
26 self
.init_config_data()
28 # These methods are best left implemented in git.py
43 'format_patch_helper',
56 setattr(self
, cmd
, getattr(git
,cmd
))
59 #####################################################
60 # Used in various places
67 git_version
= git
.version(),
69 #####################################################
70 # Used primarily by the main UI
71 project
= os
.path
.basename(os
.getcwd()),
77 window_geom
= utils
.parse_geom(
78 self
.get_global_ugit_geometry()),
80 #####################################################
81 # Used by the create branch dialog
87 #####################################################
88 # Used by the commit/repo browser
93 # These are parallel lists
98 # All items below here are re-calculated in
101 directory_entries
= {},
103 # These are also parallel lists
110 def init_config_data(self
):
111 """Reads git config --list and creates parameters
113 # These parameters are saved in .gitconfig,
114 # so ideally these should be as short as possible.
116 # config items that are controllable globally
118 self
.__local
_and
_global
_defaults
= {
121 'merge_summary': False,
122 'merge_diffstat': True,
123 'merge_verbosity': 2,
124 'gui_diffcontext': 5,
125 'gui_pruneduringfetch': False,
127 # config items that are purely git config --global settings
128 self
.__global
_defaults
= {
131 'ugit_fontui_size':12,
133 'ugit_fontdiff_size':12,
134 'ugit_historybrowser': 'gitk',
135 'ugit_savewindowsettings': False,
136 'ugit_saveatexit': False,
139 local_dict
= git
.config_dict(local
=True)
140 global_dict
= git
.config_dict(local
=False)
142 for k
,v
in local_dict
.iteritems():
143 self
.set_param('local_'+k
, v
)
144 for k
,v
in global_dict
.iteritems():
145 self
.set_param('global_'+k
, v
)
146 if k
not in local_dict
:
148 self
.set_param('local_'+k
, v
)
150 # Bootstrap the internal font*_size variables
151 for param
in ('global_ugit_fontui', 'global_ugit_fontdiff'):
152 if hasattr(self
, param
):
153 font
= self
.get_param(param
)
155 size
= int(font
.split(',')[1])
156 self
.set_param(param
+'_size', size
)
157 param
= param
[len('global_'):]
158 global_dict
[param
] = font
159 global_dict
[param
+'_size'] = size
161 # Load defaults for all undefined items
162 local_and_global_defaults
= self
.__local
_and
_global
_defaults
163 for k
,v
in local_and_global_defaults
.iteritems():
164 if k
not in local_dict
:
165 self
.set_param('local_'+k
, v
)
166 if k
not in global_dict
:
167 self
.set_param('global_'+k
, v
)
169 global_defaults
= self
.__global
_defaults
170 for k
,v
in global_defaults
.iteritems():
171 if k
not in global_dict
:
172 self
.set_param('global_'+k
, v
)
174 def save_config_param(self
, param
):
175 if param
not in self
.get_config_params():
177 value
= self
.get_param(param
)
178 if param
== 'local_gui_diffcontext':
179 git
.DIFF_CONTEXT
= value
180 if param
.startswith('local_'):
181 param
= param
[len('local_'):]
183 elif param
.startswith('global_'):
184 param
= param
[len('global_'):]
187 raise Exception("Invalid param '%s' passed to " % param
188 + "save_config_param()")
189 param
= param
.replace('_','.') # model -> git
190 return git
.config_set(param
, value
, local
=is_local
)
192 def init_browser_data(self
):
193 '''This scans over self.(names, sha1s, types) to generate
194 directories, directory_entries, and subtree_*'''
196 # Collect data for the model
197 if not self
.get_branch(): return
199 self
.subtree_types
= []
200 self
.subtree_sha1s
= []
201 self
.subtree_names
= []
202 self
.directories
= []
203 self
.directory_entries
= {}
205 # Lookup the tree info
206 tree_info
= git
.parse_ls_tree(self
.get_branch())
208 self
.set_types(map( lambda(x
): x
[1], tree_info
))
209 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
210 self
.set_names(map( lambda(x
): x
[3], tree_info
))
212 if self
.directory
: self
.directories
.append('..')
214 dir_entries
= self
.directory_entries
215 dir_regex
= re
.compile('([^/]+)/')
219 for idx
, name
in enumerate(self
.names
):
221 if not name
.startswith(self
.directory
): continue
222 name
= name
[ len(self
.directory
): ]
225 # This is a directory...
226 match
= dir_regex
.match(name
)
227 if not match
: continue
229 dirent
= match
.group(1) + '/'
230 if dirent
not in self
.directory_entries
:
231 self
.directory_entries
[dirent
] = []
233 if dirent
not in dirs_seen
:
234 dirs_seen
[dirent
] = True
235 self
.directories
.append(dirent
)
237 entry
= name
.replace(dirent
, '')
238 entry_match
= dir_regex
.match(entry
)
240 subdir
= entry_match
.group(1) + '/'
241 if subdir
in subdirs_seen
: continue
242 subdirs_seen
[subdir
] = True
243 dir_entries
[dirent
].append(subdir
)
245 dir_entries
[dirent
].append(entry
)
247 self
.subtree_types
.append(self
.types
[idx
])
248 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
249 self
.subtree_names
.append(name
)
251 def get_history_browser(self
):
252 return self
.get_param('global_ugit_historybrowser')
254 def remember_gui_settings(self
):
255 return self
.get_param('global_ugit_savewindowsettings')
257 def save_at_exit(self
):
258 return self
.get_param('global_ugit_saveatexit')
260 def get_tree_node(self
, idx
):
261 return (self
.get_types()[idx
],
262 self
.get_sha1s()[idx
],
263 self
.get_names()[idx
] )
265 def get_subtree_node(self
, idx
):
266 return (self
.get_subtree_types()[idx
],
267 self
.get_subtree_sha1s()[idx
],
268 self
.get_subtree_names()[idx
] )
270 def get_all_branches(self
):
271 return (self
.get_local_branches() + self
.get_remote_branches())
273 def set_remote(self
, remote
):
274 if not remote
: return
275 self
.set_param('remote', remote
)
276 branches
= utils
.grep( '%s/\S+$' % remote
,
277 git
.branch(remote
=True), squash
=False)
278 self
.set_remote_branches(branches
)
280 def add_signoff(self
,*rest
):
281 '''Adds a standard Signed-off by: tag to the end
282 of the current commit message.'''
284 msg
= self
.get_commitmsg()
285 signoff
=('\n\nSigned-off-by: %s <%s>\n' % (
286 self
.get_local_user_name(),
287 self
.get_local_user_email()))
289 if signoff
not in msg
:
290 self
.set_commitmsg(msg
+ signoff
)
292 def apply_diff(self
, filename
):
293 return git
.apply(filename
, index
=True, cached
=True)
295 def __get_squash_msg_path(self
):
296 return os
.path
.join(os
.getcwd(), '.git', 'SQUASH_MSG')
298 def has_squash_msg(self
):
299 squash_msg
= self
.__get
_squash
_msg
_path
()
300 return os
.path
.exists(squash_msg
)
302 def get_squash_msg(self
):
303 return utils
.slurp(self
.__get
_squash
_msg
_path
())
305 def set_squash_msg(self
):
306 self
.set_commitmsg(self
.get_squash_msg())
308 def get_prev_commitmsg(self
,*rest
):
309 '''Queries git for the latest commit message and sets it in
312 commit_lines
= git
.show('HEAD').split('\n')
313 for idx
, msg
in enumerate(commit_lines
):
316 if msg
.startswith('diff --git'):
319 commit_msg
.append(msg
)
320 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
322 def update_status(self
):
323 # This allows us to defer notification until the
324 # we finish processing data
325 notify_enabled
= self
.get_notify()
326 self
.set_notify(False)
328 # Reset the staged and unstaged model lists
329 # NOTE: the model's unstaged list is used to
330 # hold both modified and untracked files.
335 # Read git status items
338 untracked_items
) = git
.parse_status()
340 # Gather items to be committed
341 for staged
in staged_items
:
342 if staged
not in self
.get_staged():
343 self
.add_staged(staged
)
345 # Gather unindexed items
346 for modified
in modified_items
:
347 if modified
not in self
.get_modified():
348 self
.add_modified(modified
)
350 # Gather untracked items
351 for untracked
in untracked_items
:
352 if untracked
not in self
.get_untracked():
353 self
.add_untracked(untracked
)
355 self
.set_branch(git
.current_branch())
356 self
.set_unstaged(self
.get_modified() + self
.get_untracked())
357 self
.set_remotes(git
.remote().splitlines())
358 self
.set_remote_branches(git
.branch(remote
=True))
359 self
.set_local_branches(git
.branch(remote
=False))
360 self
.set_tags(git
.tag().splitlines())
361 self
.set_revision('')
362 self
.set_local_branch('')
363 self
.set_remote_branch('')
364 # Re-enable notifications and emit changes
365 self
.set_notify(notify_enabled
)
366 self
.notify_observers('staged','unstaged')
368 def delete_branch(self
, branch
):
369 return git
.branch(name
=branch
, delete
=True)
371 def get_revision_sha1(self
, idx
):
372 return self
.get_revisions()[idx
]
374 def get_config_params(self
):
376 params
.extend(map(lambda x
: 'local_' + x
,
377 self
.__local
_and
_global
_defaults
.keys()))
378 params
.extend(map(lambda x
: 'global_' + x
,
379 self
.__local
_and
_global
_defaults
.keys()))
380 params
.extend(map(lambda x
: 'global_' + x
,
381 self
.__global
_defaults
.keys()))
384 def apply_font_size(self
, param
, default
):
385 old_font
= self
.get_param(param
)
389 size
= self
.get_param(param
+'_size')
390 props
= old_font
.split(',')
392 new_font
= ','.join(props
)
394 self
.set_param(param
, new_font
)
396 def read_font_size(self
, param
, new_font
):
397 new_size
= int(new_font
.split(',')[1])
398 self
.set_param(param
, new_size
)
400 def get_commit_diff(self
, sha1
):
401 commit
= git
.show(sha1
)
402 first_newline
= commit
.index('\n')
403 if commit
[first_newline
+1:].startswith('Merge:'):
409 suppress_header
=False,
415 def get_diff_and_status(self
, idx
, staged
=True):
417 filename
= self
.get_staged()[idx
]
418 if os
.path
.exists(filename
):
419 status
= 'Staged for commit'
421 status
= 'Staged for removal'
422 diff
= self
.diff_helper(
427 filename
= self
.get_unstaged()[idx
]
428 if os
.path
.isdir(filename
):
429 status
= 'Untracked directory'
430 diff
= '\n'.join(os
.listdir(filename
))
431 elif filename
in self
.get_modified():
432 status
= 'Modified, not staged'
433 diff
= self
.diff_helper(
438 status
= 'Untracked, not staged'
440 file_type
= utils
.run_cmd('file',filename
, b
=True)
441 if 'binary' in file_type
or 'data' in file_type
:
442 diff
= utils
.run_cmd('hexdump', filename
, C
=True)
444 if os
.path
.exists(filename
):
445 file = open(filename
, 'r')
452 def stage_modified(self
):
453 output
= git
.add(self
.get_modified())
457 def stage_untracked(self
):
458 output
= git
.add(self
.get_untracked())
462 def reset(self
, *items
):
463 output
= git
.reset('--', *items
)
467 def unstage_all(self
):
468 git
.reset('--', *self
.get_staged())
471 def save_gui_settings(self
):
472 git
.config_set('ugit.geometry', utils
.get_geom(), local
=False)