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 # Import all git commands from git.py
29 for name
, cmd
in git
.commands
.iteritems():
30 setattr(self
, name
, cmd
)
33 #####################################################
34 # Used in various places
41 git_version
= git
.version(),
43 #####################################################
44 # Used primarily by the main UI
45 project
= os
.path
.basename(os
.getcwd()),
51 window_geom
= utils
.parse_geom(
52 self
.get_global_ugit_geometry()),
54 #####################################################
55 # Used by the create branch dialog
61 #####################################################
62 # Used by the commit/repo browser
67 # These are parallel lists
72 # All items below here are re-calculated in
75 directory_entries
= {},
77 # These are also parallel lists
84 def init_config_data(self
):
85 """Reads git config --list and creates parameters
87 # These parameters are saved in .gitconfig,
88 # so ideally these should be as short as possible.
90 # config items that are controllable globally
92 self
.__local
_and
_global
_defaults
= {
95 'merge_summary': False,
96 'merge_diffstat': True,
99 'gui_pruneduringfetch': False,
101 # config items that are purely git config --global settings
102 self
.__global
_defaults
= {
105 'ugit_fontui_size':12,
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
:
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
)
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():
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_'):]
157 elif param
.startswith('global_'):
158 param
= param
[len('global_'):]
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('([^/]+)/')
193 for idx
, name
in enumerate(self
.names
):
195 if not name
.startswith(self
.directory
): continue
196 name
= name
[ len(self
.directory
): ]
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
)
214 subdir
= entry_match
.group(1) + '/'
215 if subdir
in subdirs_seen
: continue
216 subdirs_seen
[subdir
] = True
217 dir_entries
[dirent
].append(subdir
)
219 dir_entries
[dirent
].append(entry
)
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
286 commit_lines
= git
.show('HEAD').split('\n')
287 for idx
, msg
in enumerate(commit_lines
):
290 if msg
.startswith('diff --git'):
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.
309 # Read git status 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
):
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()))
358 def apply_font_size(self
, param
, default
):
359 old_font
= self
.get_param(param
)
363 size
= self
.get_param(param
+'_size')
364 props
= old_font
.split(',')
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:'):
383 suppress_header
=False,
389 def get_diff_and_status(self
, idx
, staged
=True):
391 filename
= self
.get_staged()[idx
]
392 if os
.path
.exists(filename
):
393 status
= 'Staged for commit'
395 status
= 'Staged for removal'
396 diff
= self
.diff_helper(
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(
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)
418 if os
.path
.exists(filename
):
419 file = open(filename
, 'r')
426 def stage_modified(self
):
427 output
= git
.add(self
.get_modified())
431 def stage_untracked(self
):
432 output
= git
.add(self
.get_untracked())
436 def reset(self
, *items
):
437 output
= git
.reset('--', *items
)
441 def unstage_all(self
):
442 git
.reset('--', *self
.get_staged())
445 def save_gui_settings(self
):
446 git
.config_set('ugit.geometry', utils
.get_geom(), local
=False)