maint: remove dead code
[git-cola.git] / cola / models / main.py
blobe67baeb5de6a9e9ab73d222cae835fe2bc97ee64
1 # Copyright (C) 2007-2018 David Aguilar
2 """This module provides the central cola model.
3 """
4 from __future__ import division, absolute_import, unicode_literals
6 import os
8 from .. import core
9 from .. import gitcmds
10 from ..git import STDOUT
11 from ..observable import Observable
12 from . import prefs
15 def create(context):
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.
25 # Observable messages
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'
37 # States
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))
54 unstaged = property(
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
68 self.annex = False
69 self.lfs = False
70 self.head = 'HEAD'
71 self.diff_text = ''
72 self.diff_type = 'text' # text, image
73 self.mode = self.mode_none
74 self.filename = None
75 self.is_merging = False
76 self.is_rebasing = False
77 self.currentbranch = ''
78 self.directory = ''
79 self.project = ''
80 self.remotes = []
81 self.filter_paths = None
82 self.images = []
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
89 self.staged = []
90 self.untracked = []
91 self.unmerged = []
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 = []
99 self.tags = []
100 if cwd:
101 self.set_worktree(cwd)
103 def unstageable(self):
104 return self.mode in self.modes_unstageable
106 def amending(self):
107 return self.mode == self.mode_amend
109 def undoable(self):
110 """Whether we can checkout files from the $head."""
111 return self.mode in self.modes_undoable
113 def stageable(self):
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()
123 if is_valid:
124 cwd = self.git.getcwd()
125 self.project = os.path.basename(cwd)
126 self.set_directory(cwd)
127 core.chdir(cwd)
128 self.update_config(reset=True)
129 return is_valid
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')
142 return (lfs_filter
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):
147 self.commitmsg = msg
148 if notify:
149 self.notify_observers(self.message_commit_message_changed, msg)
151 def save_commitmsg(self, msg=None):
152 if msg is None:
153 msg = self.commitmsg
154 path = self.git.git_path('GIT_COLA_MSG')
155 try:
156 if not msg.endswith('\n'):
157 msg += '\n'
158 core.write(path, msg)
159 except (OSError, IOError):
160 pass
161 return path
163 def set_diff_text(self, txt):
164 """Update the text displayed in the diff editor"""
165 changed = txt != self.diff_text
166 self.diff_text = txt
167 self.notify_observers(self.message_diff_text_updated, txt)
168 if changed:
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"""
178 self.images = images
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):
189 if self.amending():
190 if mode != self.mode_none:
191 return
192 if self.is_merging and mode == self.mode_amend:
193 mode = self.mode
194 if mode == self.mode_amend:
195 head = 'HEAD^'
196 else:
197 head = 'HEAD'
198 self.notify_observers(self.message_mode_about_to_change, mode)
199 self.head = head
200 self.mode = 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()
227 self.update_config()
228 self.emit_updated()
230 def update_config(self, emit=False, reset=False):
231 if reset:
232 self.cfg.reset()
233 self.annex = self.cfg.is_annex()
234 self.lfs = self.is_git_lfs_enabled()
235 if emit:
236 self.emit_updated()
238 def update_files(self, update_index=False, emit=False):
239 self._update_files(update_index=update_index)
240 if emit:
241 self.emit_updated()
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
259 if self.is_empty():
260 selection.reset()
261 else:
262 selection.update(self)
263 if selection.is_empty():
264 self.set_diff_text('')
266 def is_empty(self):
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(
283 context, split=True)
284 self.local_branches = local_branches
285 self.remote_branches = remote_branches
286 self.tags = tags
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
302 if self.amending():
303 return
304 # Check if there's a message file in .git/
305 context = self.context
306 merge_msg_path = gitcmds.merge_message_path(context)
307 if merge_msg_path:
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()
332 self.emit_updated()
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)"""
361 if not revs:
362 return []
363 outs = []
364 errs = []
365 status = 0
366 for rev in revs:
367 stat, out, err = self.git.cherry_pick(rev)
368 status = max(stat, status)
369 outs.append(out)
370 errs.append(err)
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
383 def getcwd(self):
384 """If we've chosen a directory then use it, otherwise use current"""
385 if self.directory:
386 return self.directory
387 return core.getcwd()
390 # Helpers
391 def remote_args(remote,
392 local_branch='',
393 remote_branch='',
394 ff_only=False,
395 force=False,
396 no_ff=False,
397 tags=False,
398 rebase=False,
399 pull=False,
400 push=False,
401 set_upstream=False,
402 prune=False):
403 """Return arguments for git fetch/push/pull"""
405 args = [remote]
406 what = refspec_arg(local_branch, remote_branch, pull, push)
407 if what:
408 args.append(what)
410 kwargs = {
411 'verbose': True,
413 if pull:
414 if rebase:
415 kwargs['rebase'] = True
416 elif ff_only:
417 kwargs['ff_only'] = True
418 elif no_ff:
419 kwargs['no_ff'] = True
420 elif force:
421 kwargs['force'] = True
423 if push and set_upstream:
424 kwargs['set_upstream'] = True
425 if tags:
426 kwargs['tags'] = True
427 if prune:
428 kwargs['prune'] = True
430 return (args, kwargs)
433 def refspec(src, dst, push=False):
434 if push and src == dst:
435 spec = src
436 else:
437 spec = '%s:%s' % (src, dst)
438 return spec
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)
445 else:
446 what = local_branch or remote_branch or None
447 return what
450 def run_remote_action(action, remote, **kwargs):
451 args, kwargs = remote_args(remote, **kwargs)
452 return action(*args, **kwargs)