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',
53 setattr(self
, cmd
, getattr(git
,cmd
))
56 #####################################################
57 # Used in various places
64 git_version
= git
.version(),
66 #####################################################
67 # Used primarily by the main UI
68 project
= os
.path
.basename(os
.getcwd()),
74 window_geom
= utils
.parse_geom(
75 self
.get_global_ugit_geometry()),
77 #####################################################
78 # Used by the create branch dialog
84 #####################################################
85 # Used by the commit/repo browser
90 # These are parallel lists
95 # All items below here are re-calculated in
98 directory_entries
= {},
100 # These are also parallel lists
107 def init_config_data(self
):
108 """Reads git config --list and creates parameters
110 # These parameters are saved in .gitconfig,
111 # so ideally these should be as short as possible.
113 # config items that are controllable globally
115 self
.__local
_and
_global
_defaults
= {
118 'merge_summary': False,
119 'merge_diffstat': True,
120 'merge_verbosity': 2,
121 'gui_diffcontext': 5,
122 'gui_pruneduringfetch': False,
124 # config items that are purely git config --global settings
125 self
.__global
_defaults
= {
128 'ugit_fontui_size':12,
130 'ugit_fontdiff_size':12,
131 'ugit_historybrowser': 'gitk',
132 'ugit_savewindowsettings': False,
133 'ugit_saveatexit': False,
136 local_dict
= git
.config_dict(local
=True)
137 global_dict
= git
.config_dict(local
=False)
139 for k
,v
in local_dict
.iteritems():
140 self
.set_param('local_'+k
, v
)
141 for k
,v
in global_dict
.iteritems():
142 self
.set_param('global_'+k
, v
)
143 if k
not in local_dict
:
145 self
.set_param('local_'+k
, v
)
147 # Bootstrap the internal font*_size variables
148 for param
in ('global_ugit_fontui', 'global_ugit_fontdiff'):
149 if hasattr(self
, param
):
150 font
= self
.get_param(param
)
152 size
= int(font
.split(',')[1])
153 self
.set_param(param
+'_size', size
)
154 param
= param
[len('global_'):]
155 global_dict
[param
] = font
156 global_dict
[param
+'_size'] = size
158 # Load defaults for all undefined items
159 local_and_global_defaults
= self
.__local
_and
_global
_defaults
160 for k
,v
in local_and_global_defaults
.iteritems():
161 if k
not in local_dict
:
162 self
.set_param('local_'+k
, v
)
163 if k
not in global_dict
:
164 self
.set_param('global_'+k
, v
)
166 global_defaults
= self
.__global
_defaults
167 for k
,v
in global_defaults
.iteritems():
168 if k
not in global_dict
:
169 self
.set_param('global_'+k
, v
)
171 def save_config_param(self
, param
):
172 if param
not in self
.get_config_params():
174 value
= self
.get_param(param
)
175 if param
== 'local_gui_diffcontext':
176 git
.DIFF_CONTEXT
= value
177 if param
.startswith('local_'):
178 param
= param
[len('local_'):]
180 elif param
.startswith('global_'):
181 param
= param
[len('global_'):]
184 raise Exception("Invalid param '%s' passed to " % param
185 + "save_config_param()")
186 param
= param
.replace('_','.') # model -> git
187 return git
.config_set(param
, value
, local
=is_local
)
189 def init_browser_data(self
):
190 '''This scans over self.(names, sha1s, types) to generate
191 directories, directory_entries, and subtree_*'''
193 # Collect data for the model
194 if not self
.get_branch(): return
196 self
.subtree_types
= []
197 self
.subtree_sha1s
= []
198 self
.subtree_names
= []
199 self
.directories
= []
200 self
.directory_entries
= {}
202 # Lookup the tree info
203 tree_info
= git
.parse_ls_tree(self
.get_branch())
205 self
.set_types(map( lambda(x
): x
[1], tree_info
))
206 self
.set_sha1s(map( lambda(x
): x
[2], tree_info
))
207 self
.set_names(map( lambda(x
): x
[3], tree_info
))
209 if self
.directory
: self
.directories
.append('..')
211 dir_entries
= self
.directory_entries
212 dir_regex
= re
.compile('([^/]+)/')
216 for idx
, name
in enumerate(self
.names
):
218 if not name
.startswith(self
.directory
): continue
219 name
= name
[ len(self
.directory
): ]
222 # This is a directory...
223 match
= dir_regex
.match(name
)
224 if not match
: continue
226 dirent
= match
.group(1) + '/'
227 if dirent
not in self
.directory_entries
:
228 self
.directory_entries
[dirent
] = []
230 if dirent
not in dirs_seen
:
231 dirs_seen
[dirent
] = True
232 self
.directories
.append(dirent
)
234 entry
= name
.replace(dirent
, '')
235 entry_match
= dir_regex
.match(entry
)
237 subdir
= entry_match
.group(1) + '/'
238 if subdir
in subdirs_seen
: continue
239 subdirs_seen
[subdir
] = True
240 dir_entries
[dirent
].append(subdir
)
242 dir_entries
[dirent
].append(entry
)
244 self
.subtree_types
.append(self
.types
[idx
])
245 self
.subtree_sha1s
.append(self
.sha1s
[idx
])
246 self
.subtree_names
.append(name
)
248 def get_history_browser(self
):
249 return self
.get_param('global_ugit_historybrowser')
251 def remember_gui_settings(self
):
252 return self
.get_param('global_ugit_savewindowsettings')
254 def save_at_exit(self
):
255 return self
.get_param('global_ugit_saveatexit')
257 def get_tree_node(self
, idx
):
258 return (self
.get_types()[idx
],
259 self
.get_sha1s()[idx
],
260 self
.get_names()[idx
] )
262 def get_subtree_node(self
, idx
):
263 return (self
.get_subtree_types()[idx
],
264 self
.get_subtree_sha1s()[idx
],
265 self
.get_subtree_names()[idx
] )
267 def get_all_branches(self
):
268 return (self
.get_local_branches() + self
.get_remote_branches())
270 def set_remote(self
, remote
):
271 if not remote
: return
272 self
.set_param('remote', remote
)
273 branches
= utils
.grep( '%s/\S+$' % remote
,
274 git
.branch(remote
=True), squash
=False)
275 self
.set_remote_branches(branches
)
277 def add_signoff(self
,*rest
):
278 '''Adds a standard Signed-off by: tag to the end
279 of the current commit message.'''
281 msg
= self
.get_commitmsg()
282 signoff
=('\n\nSigned-off-by: %s <%s>\n' % (
283 self
.get_local_user_name(),
284 self
.get_local_user_email()))
286 if signoff
not in msg
:
287 self
.set_commitmsg(msg
+ signoff
)
289 def apply_diff(self
, filename
):
290 return git
.apply(filename
, index
=True, cached
=True)
292 def __get_squash_msg_path(self
):
293 return os
.path
.join(os
.getcwd(), '.git', 'SQUASH_MSG')
295 def has_squash_msg(self
):
296 squash_msg
= self
.__get
_squash
_msg
_path
()
297 return os
.path
.exists(squash_msg
)
299 def get_squash_msg(self
):
300 return utils
.slurp(self
.__get
_squash
_msg
_path
())
302 def set_squash_msg(self
):
303 self
.set_commitmsg(self
.get_squash_msg())
305 def get_prev_commitmsg(self
,*rest
):
306 '''Queries git for the latest commit message and sets it in
309 commit_lines
= git
.show('HEAD').split('\n')
310 for idx
, msg
in enumerate(commit_lines
):
313 if msg
.startswith('diff --git'):
316 commit_msg
.append(msg
)
317 self
.set_commitmsg('\n'.join(commit_msg
).rstrip())
319 def update_status(self
):
320 # This allows us to defer notification until the
321 # we finish processing data
322 notify_enabled
= self
.get_notify()
323 self
.set_notify(False)
325 # Reset the staged and unstaged model lists
326 # NOTE: the model's unstaged list is used to
327 # hold both modified and untracked files.
332 # Read git status items
335 untracked_items
) = git
.parse_status()
337 # Gather items to be committed
338 for staged
in staged_items
:
339 if staged
not in self
.get_staged():
340 self
.add_staged(staged
)
342 # Gather unindexed items
343 for modified
in modified_items
:
344 if modified
not in self
.get_modified():
345 self
.add_modified(modified
)
347 # Gather untracked items
348 for untracked
in untracked_items
:
349 if untracked
not in self
.get_untracked():
350 self
.add_untracked(untracked
)
352 self
.set_branch(git
.current_branch())
353 self
.set_unstaged(self
.get_modified() + self
.get_untracked())
354 self
.set_remotes(git
.remote().splitlines())
355 self
.set_remote_branches(git
.branch(remote
=True))
356 self
.set_local_branches(git
.branch(remote
=False))
357 self
.set_tags(git
.tag().splitlines())
358 self
.set_revision('')
359 self
.set_local_branch('')
360 self
.set_remote_branch('')
361 # Re-enable notifications and emit changes
362 self
.set_notify(notify_enabled
)
363 self
.notify_observers('staged','unstaged')
365 def delete_branch(self
, branch
):
366 return git
.branch(name
=branch
, delete
=True)
368 def get_revision_sha1(self
, idx
):
369 return self
.get_revisions()[idx
]
371 def get_config_params(self
):
373 params
.extend(map(lambda x
: 'local_' + x
,
374 self
.__local
_and
_global
_defaults
.keys()))
375 params
.extend(map(lambda x
: 'global_' + x
,
376 self
.__local
_and
_global
_defaults
.keys()))
377 params
.extend(map(lambda x
: 'global_' + x
,
378 self
.__global
_defaults
.keys()))
381 def apply_font_size(self
, param
, default
):
382 old_font
= self
.get_param(param
)
386 size
= self
.get_param(param
+'_size')
387 props
= old_font
.split(',')
389 new_font
= ','.join(props
)
391 self
.set_param(param
, new_font
)
393 def read_font_size(self
, param
, new_font
):
394 new_size
= int(new_font
.split(',')[1])
395 self
.set_param(param
, new_size
)
397 def get_commit_diff(self
, sha1
):
398 commit
= git
.show(sha1
)
399 first_newline
= commit
.index('\n')
400 if commit
[first_newline
+1:].startswith('Merge:'):
406 suppress_header
=False,
412 def get_diff_and_status(self
, idx
, staged
=True):
414 filename
= self
.get_staged()[idx
]
415 if os
.path
.exists(filename
):
416 status
= 'Staged for commit'
418 status
= 'Staged for removal'
419 diff
= self
.diff_helper(
424 filename
= self
.get_unstaged()[idx
]
425 if os
.path
.isdir(filename
):
426 status
= 'Untracked directory'
427 diff
= '\n'.join(os
.listdir(filename
))
428 elif filename
in self
.get_modified():
429 status
= 'Modified, not staged'
430 diff
= self
.diff_helper(
435 status
= 'Untracked, not staged'
437 file_type
= utils
.run_cmd('file',filename
, b
=True)
438 if 'binary' in file_type
or 'data' in file_type
:
439 diff
= utils
.run_cmd('hexdump', filename
, C
=True)
441 if os
.path
.exists(filename
):
442 file = open(filename
, 'r')
449 def stage_modified(self
):
450 output
= git
.add(self
.get_modified())
454 def stage_untracked(self
):
455 output
= git
.add(self
.get_untracked())
459 def reset(self
, *items
):
460 output
= git
.reset('--', *items
)
464 def unstage_all(self
):
465 git
.reset('--', *self
.get_staged())
468 def save_gui_settings(self
):
469 git
.config_set('ugit.geometry', utils
.get_geom(), local
=False)