1 # Copyright (C) 2007-2018 David Aguilar
2 """This module provides the central cola model.
4 from __future__
import division
, absolute_import
, unicode_literals
10 from ..git
import STDOUT
11 from ..observable
import Observable
16 """Create the repository status model"""
17 return MainModel(context
)
20 class MainModel(Observable
):
21 """Repository status model"""
22 # TODO this class can probably be split apart into a DiffModel,
23 # CommitMessageModel, StatusModel, and an AppStatusStateMachine.
26 message_about_to_update
= 'about_to_update'
27 message_commit_message_changed
= 'commit_message_changed'
28 message_diff_text_changed
= 'diff_text_changed'
29 message_diff_text_updated
= 'diff_text_updated'
30 message_diff_type_changed
= 'diff_type_changed'
31 message_filename_changed
= 'filename_changed'
32 message_images_changed
= 'images_changed'
33 message_mode_about_to_change
= 'mode_about_to_change'
34 message_mode_changed
= 'mode_changed'
35 message_updated
= 'updated'
38 mode_none
= 'none' # Default: nothing's happened, do nothing
39 mode_worktree
= 'worktree' # Comparing index to worktree
40 mode_diffstat
= 'diffstat' # Showing a diffstat
41 mode_untracked
= 'untracked' # Dealing with an untracked file
42 mode_index
= 'index' # Comparing index to last commit
43 mode_amend
= 'amend' # Amending a commit
45 # Modes where we can checkout files from the $head
46 modes_undoable
= set((mode_amend
, mode_index
, mode_worktree
))
48 # Modes where we can partially stage files
49 modes_stageable
= set((mode_amend
, mode_worktree
, mode_untracked
))
51 # Modes where we can partially unstage files
52 modes_unstageable
= set((mode_amend
, mode_index
))
55 lambda self
: self
.modified
+ self
.unmerged
+ self
.untracked
)
56 """An aggregate of the modified, unmerged, and untracked file lists."""
58 def __init__(self
, context
, cwd
=None):
59 """Interface to the main repository status"""
60 Observable
.__init
__(self
)
62 self
.context
= context
63 self
.git
= context
.git
64 self
.cfg
= context
.cfg
65 self
.selection
= context
.selection
67 self
.initialized
= False
72 self
.diff_type
= 'text' # text, image
73 self
.mode
= self
.mode_none
75 self
.is_merging
= False
76 self
.is_rebasing
= False
77 self
.currentbranch
= ''
81 self
.filter_paths
= None
84 self
.commitmsg
= '' # current commit message
85 self
._auto
_commitmsg
= '' # e.g. .git/MERGE_MSG
86 self
._prev
_commitmsg
= '' # saved here when clobbered by .git/MERGE_MSG
88 self
.modified
= [] # modified, staged, untracked, unmerged paths
92 self
.upstream_changed
= [] # paths that've changed upstream
93 self
.staged_deleted
= set()
94 self
.unstaged_deleted
= set()
95 self
.submodules
= set()
97 self
.local_branches
= []
98 self
.remote_branches
= []
101 self
.set_worktree(cwd
)
103 def unstageable(self
):
104 return self
.mode
in self
.modes_unstageable
107 return self
.mode
== self
.mode_amend
110 """Whether we can checkout files from the $head."""
111 return self
.mode
in self
.modes_undoable
114 """Whether staging should be allowed."""
115 return self
.mode
in self
.modes_stageable
117 def all_branches(self
):
118 return self
.local_branches
+ self
.remote_branches
120 def set_worktree(self
, worktree
):
121 self
.git
.set_worktree(worktree
)
122 is_valid
= self
.git
.is_valid()
124 cwd
= self
.git
.getcwd()
125 self
.project
= os
.path
.basename(cwd
)
126 self
.set_directory(cwd
)
128 self
.update_config(reset
=True)
131 def is_git_lfs_enabled(self
):
132 """Return True if `git lfs install` has been run
134 We check for the existence of the "lfs" object-storea, and one of the
135 "git lfs install"-provided hooks. This allows us to detect when
136 "git lfs uninstall" has been run.
139 lfs_filter
= self
.cfg
.get('filter.lfs.clean', default
=False)
140 lfs_dir
= lfs_filter
and self
.git
.git_path('lfs')
141 lfs_hook
= lfs_filter
and self
.git
.git_path('hooks', 'post-merge')
143 and lfs_dir
and core
.exists(lfs_dir
)
144 and lfs_hook
and core
.exists(lfs_hook
))
146 def set_commitmsg(self
, msg
, notify
=True):
149 self
.notify_observers(self
.message_commit_message_changed
, msg
)
151 def save_commitmsg(self
, msg
=None):
154 path
= self
.git
.git_path('GIT_COLA_MSG')
156 if not msg
.endswith('\n'):
158 core
.write(path
, msg
)
159 except (OSError, IOError):
163 def set_diff_text(self
, txt
):
164 """Update the text displayed in the diff editor"""
165 changed
= txt
!= self
.diff_text
167 self
.notify_observers(self
.message_diff_text_updated
, txt
)
169 self
.notify_observers(self
.message_diff_text_changed
)
171 def set_diff_type(self
, diff_type
): # text, image
172 """Set the diff type to either text or image"""
173 self
.diff_type
= diff_type
174 self
.notify_observers(self
.message_diff_type_changed
, diff_type
)
176 def set_images(self
, images
):
177 """Update the images shown in the preview pane"""
179 self
.notify_observers(self
.message_images_changed
, images
)
181 def set_directory(self
, path
):
182 self
.directory
= path
184 def set_filename(self
, filename
):
185 self
.filename
= filename
186 self
.notify_observers(self
.message_filename_changed
, filename
)
188 def set_mode(self
, mode
):
190 if mode
!= self
.mode_none
:
192 if self
.is_merging
and mode
== self
.mode_amend
:
194 if mode
== self
.mode_amend
:
198 self
.notify_observers(self
.message_mode_about_to_change
, mode
)
201 self
.notify_observers(self
.message_mode_changed
, mode
)
203 def update_path_filter(self
, filter_paths
):
204 self
.filter_paths
= filter_paths
205 self
.update_file_status()
207 def emit_about_to_update(self
):
208 self
.notify_observers(self
.message_about_to_update
)
210 def emit_updated(self
):
211 self
.notify_observers(self
.message_updated
)
213 def update_file_status(self
, update_index
=False):
214 self
.emit_about_to_update()
215 self
.update_files(update_index
=update_index
, emit
=True)
217 def update_status(self
, update_index
=False):
218 # Give observers a chance to respond
219 self
.emit_about_to_update()
220 self
.initialized
= True
221 self
._update
_merge
_rebase
_status
()
222 self
._update
_files
(update_index
=update_index
)
223 self
._update
_remotes
()
224 self
._update
_branches
_and
_tags
()
225 self
._update
_branch
_heads
()
226 self
._update
_commitmsg
()
230 def update_config(self
, emit
=False, reset
=False):
233 self
.annex
= self
.cfg
.is_annex()
234 self
.lfs
= self
.is_git_lfs_enabled()
238 def update_files(self
, update_index
=False, emit
=False):
239 self
._update
_files
(update_index
=update_index
)
243 def _update_files(self
, update_index
=False):
244 context
= self
.context
245 display_untracked
= prefs
.display_untracked(context
)
246 state
= gitcmds
.worktree_state(
247 context
, head
=self
.head
, update_index
=update_index
,
248 display_untracked
=display_untracked
, paths
=self
.filter_paths
)
249 self
.staged
= state
.get('staged', [])
250 self
.modified
= state
.get('modified', [])
251 self
.unmerged
= state
.get('unmerged', [])
252 self
.untracked
= state
.get('untracked', [])
253 self
.upstream_changed
= state
.get('upstream_changed', [])
254 self
.staged_deleted
= state
.get('staged_deleted', set())
255 self
.unstaged_deleted
= state
.get('unstaged_deleted', set())
256 self
.submodules
= state
.get('submodules', set())
258 selection
= self
.selection
262 selection
.update(self
)
263 if selection
.is_empty():
264 self
.set_diff_text('')
267 return not(bool(self
.staged
or self
.modified
or
268 self
.unmerged
or self
.untracked
))
270 def is_empty_repository(self
):
271 return not self
.local_branches
273 def _update_remotes(self
):
274 self
.remotes
= self
.git
.remote()[STDOUT
].splitlines()
276 def _update_branch_heads(self
):
277 # Set these early since they are used to calculate 'upstream_changed'.
278 self
.currentbranch
= gitcmds
.current_branch(self
.context
)
280 def _update_branches_and_tags(self
):
281 context
= self
.context
282 local_branches
, remote_branches
, tags
= gitcmds
.all_refs(
284 self
.local_branches
= local_branches
285 self
.remote_branches
= remote_branches
288 def _update_merge_rebase_status(self
):
289 merge_head
= self
.git
.git_path('MERGE_HEAD')
290 rebase_merge
= self
.git
.git_path('rebase-merge')
291 self
.is_merging
= merge_head
and core
.exists(merge_head
)
292 self
.is_rebasing
= rebase_merge
and core
.exists(rebase_merge
)
293 if self
.is_merging
and self
.mode
== self
.mode_amend
:
294 self
.set_mode(self
.mode_none
)
296 def _update_commitmsg(self
):
297 """Check for merge message files and update the commit message
299 The message is cleared when the merge completes
304 # Check if there's a message file in .git/
305 context
= self
.context
306 merge_msg_path
= gitcmds
.merge_message_path(context
)
308 msg
= core
.read(merge_msg_path
)
309 if msg
!= self
._auto
_commitmsg
:
310 self
._auto
_commitmsg
= msg
311 self
._prev
_commitmsg
= self
.commitmsg
312 self
.set_commitmsg(msg
)
314 elif self
._auto
_commitmsg
and self
._auto
_commitmsg
== self
.commitmsg
:
315 self
._auto
_commitmsg
= ''
316 self
.set_commitmsg(self
._prev
_commitmsg
)
318 def update_remotes(self
):
319 self
._update
_remotes
()
320 self
._update
_branches
_and
_tags
()
322 def delete_branch(self
, branch
):
323 status
, out
, err
= self
.git
.branch(branch
, D
=True)
324 self
._update
_branches
_and
_tags
()
325 return status
, out
, err
327 def rename_branch(self
, branch
, new_branch
):
328 status
, out
, err
= self
.git
.branch(branch
, new_branch
, M
=True)
329 self
.emit_about_to_update()
330 self
._update
_branches
_and
_tags
()
331 self
._update
_branch
_heads
()
333 return status
, out
, err
335 def remote_url(self
, name
, action
):
336 push
= action
== 'push'
337 return gitcmds
.remote_url(self
.context
, name
, push
=push
)
339 def fetch(self
, remote
, **opts
):
340 return run_remote_action(self
.git
.fetch
, remote
, **opts
)
342 def push(self
, remote
, remote_branch
='', local_branch
='', **opts
):
343 # Swap the branches in push mode (reverse of fetch)
344 opts
.update(dict(local_branch
=remote_branch
,
345 remote_branch
=local_branch
))
346 return run_remote_action(self
.git
.push
, remote
, push
=True, **opts
)
348 def pull(self
, remote
, **opts
):
349 return run_remote_action(self
.git
.pull
, remote
, pull
=True, **opts
)
351 def create_branch(self
, name
, base
, track
=False, force
=False):
352 """Create a branch named 'name' from revision 'base'
354 Pass track=True to create a local tracking branch.
356 return self
.git
.branch(name
, base
, track
=track
, force
=force
)
358 def cherry_pick_list(self
, revs
):
359 """Cherry-picks each revision into the current branch.
360 Returns a list of command output strings (1 per cherry pick)"""
367 stat
, out
, err
= self
.git
.cherry_pick(rev
)
368 status
= max(stat
, status
)
371 return (status
, '\n'.join(outs
), '\n'.join(errs
))
373 def is_commit_published(self
):
374 """Return True if the latest commit exists in any remote branch"""
375 return bool(self
.git
.branch(r
=True, contains
='HEAD')[STDOUT
])
377 def untrack_paths(self
, paths
):
378 context
= self
.context
379 status
, out
, err
= gitcmds
.untrack_paths(context
, paths
)
380 self
.update_file_status()
381 return status
, out
, err
384 """If we've chosen a directory then use it, otherwise use current"""
386 return self
.directory
391 def remote_args(remote
,
403 """Return arguments for git fetch/push/pull"""
406 what
= refspec_arg(local_branch
, remote_branch
, pull
, push
)
415 kwargs
['rebase'] = True
417 kwargs
['ff_only'] = True
419 kwargs
['no_ff'] = True
421 kwargs
['force'] = True
423 if push
and set_upstream
:
424 kwargs
['set_upstream'] = True
426 kwargs
['tags'] = True
428 kwargs
['prune'] = True
430 return (args
, kwargs
)
433 def refspec(src
, dst
, push
=False):
434 if push
and src
== dst
:
437 spec
= '%s:%s' % (src
, dst
)
441 def refspec_arg(local_branch
, remote_branch
, pull
, push
):
442 """Return the refspec for a fetch or pull command"""
443 if not pull
and local_branch
and remote_branch
:
444 what
= refspec(remote_branch
, local_branch
, push
=push
)
446 what
= local_branch
or remote_branch
or None
450 def run_remote_action(action
, remote
, **kwargs
):
451 args
, kwargs
= remote_args(remote
, **kwargs
)
452 return action(*args
, **kwargs
)