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
49 setattr(self
, cmd
, getattr(git
,cmd
))
52 #####################################################
53 # Used in various places
60 git_version
= git
.version(),
62 #####################################################
63 # Used primarily by the main UI
64 project
= os
.path
.basename(os
.getcwd()),
70 window_geom
= utils
.parse_geom(
71 self
.get_global_ugit_geometry()),
73 #####################################################
74 # Used by the create branch dialog
80 #####################################################
81 # Used by the commit/repo browser
86 # These are parallel lists
91 # All items below here are re-calculated in
94 directory_entries
= {},
96 # These are also parallel lists
103 def init_config_data(self
):
104 """Reads git config --list and creates parameters
106 # These parameters are saved in .gitconfig,
107 # so ideally these should be as short as possible.
109 # config items that are controllable globally
111 self
.__local
_and
_global
_defaults
= {
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
= {
124 'ugit_fontui_size':12,
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
:
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
)
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():
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_'):]
176 elif param
.startswith('global_'):
177 param
= param
[len('global_'):]
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('([^/]+)/')
212 for idx
, name
in enumerate(self
.names
):
214 if not name
.startswith(self
.directory
): continue
215 name
= name
[ len(self
.directory
): ]
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
)
233 subdir
= entry_match
.group(1) + '/'
234 if subdir
in subdirs_seen
: continue
235 subdirs_seen
[subdir
] = True
236 dir_entries
[dirent
].append(subdir
)
238 dir_entries
[dirent
].append(entry
)
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
305 commit_lines
= git
.show('HEAD').split('\n')
306 for idx
, msg
in enumerate(commit_lines
):
309 if msg
.startswith('diff --git'):
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.
328 # Read git status 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
):
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()))
377 def apply_font_size(self
, param
, default
):
378 old_font
= self
.get_param(param
)
382 size
= self
.get_param(param
+'_size')
383 props
= old_font
.split(',')
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:'):
402 suppress_header
=False,
408 def get_diff_and_status(self
, idx
, staged
=True):
410 filename
= self
.get_staged()[idx
]
411 if os
.path
.exists(filename
):
412 status
= 'Staged for commit'
414 status
= 'Staged for removal'
415 diff
= self
.diff_helper(
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(
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)
437 if os
.path
.exists(filename
):
438 file = open(filename
, 'r')
445 def stage_modified(self
):
446 output
= git
.add(self
.get_modified())
450 def stage_untracked(self
):
451 output
= git
.add(self
.get_untracked())
455 def reset(self
, *items
):
456 output
= git
.reset('--', *items
)
460 def unstage_all(self
):
461 git
.reset('--', *self
.get_staged())
464 def save_gui_settings(self
):
465 git
.config_set('ugit.geometry', utils
.get_geom(), local
=False)