1 from __future__
import division
, absolute_import
, unicode_literals
4 from functools
import partial
6 from qtpy
.QtCore
import Qt
7 from qtpy
.QtCore
import Signal
8 from qtpy
import QtCore
9 from qtpy
import QtWidgets
12 from ..models
import prefs
13 from ..models
import selection
14 from ..widgets
import gitignore
15 from ..widgets
import standard
16 from ..qtutils
import get
17 from ..settings
import Settings
18 from .. import actions
21 from .. import hotkeys
23 from .. import qtutils
26 from . import completion
31 # Top-level status widget item indexes.
39 # Indexes into the saved_selection entries.
46 class StatusWidget(QtWidgets
.QFrame
):
48 Provides a git-status-like repository widget.
50 This widget observes the main model and broadcasts
55 def __init__(self
, context
, titlebar
, parent
):
56 QtWidgets
.QFrame
.__init
__(self
, parent
)
57 self
.context
= context
59 tooltip
= N_('Toggle the paths filter')
60 icon
= icons
.ellipsis()
61 self
.filter_button
= qtutils
.create_action_button(tooltip
=tooltip
, icon
=icon
)
62 self
.filter_widget
= StatusFilterWidget(context
)
63 self
.filter_widget
.hide()
64 self
.tree
= StatusTreeWidget(context
, parent
=self
)
65 self
.setFocusProxy(self
.tree
)
67 self
.main_layout
= qtutils
.vbox(
68 defs
.no_margin
, defs
.no_spacing
, self
.filter_widget
, self
.tree
70 self
.setLayout(self
.main_layout
)
72 self
.toggle_action
= qtutils
.add_action(
73 self
, tooltip
, self
.toggle_filter
, hotkeys
.FILTER
76 titlebar
.add_corner_widget(self
.filter_button
)
77 qtutils
.connect_button(self
.filter_button
, self
.toggle_filter
)
79 def toggle_filter(self
):
80 shown
= not self
.filter_widget
.isVisible()
81 self
.filter_widget
.setVisible(shown
)
83 self
.filter_widget
.setFocus()
87 def set_initial_size(self
):
88 self
.setMaximumWidth(222)
89 QtCore
.QTimer
.singleShot(1, lambda: self
.setMaximumWidth(2 ** 13))
92 self
.tree
.show_selection()
94 def set_filter(self
, txt
):
95 self
.filter_widget
.setVisible(True)
96 self
.filter_widget
.text
.set_value(txt
)
97 self
.filter_widget
.apply_filter()
103 self
.tree
.move_down()
105 def select_header(self
):
106 self
.tree
.select_header()
109 # pylint: disable=too-many-ancestors
110 class StatusTreeWidget(QtWidgets
.QTreeWidget
):
112 about_to_update
= Signal()
113 set_previous_contents
= Signal(list, list, list, list)
115 diff_text_changed
= Signal()
117 # Read-only access to the mode state
118 mode
= property(lambda self
: self
.m
.mode
)
120 def __init__(self
, context
, parent
=None):
121 QtWidgets
.QTreeWidget
.__init
__(self
, parent
)
122 self
.context
= context
123 self
.selection_model
= context
.selection
125 self
.setSelectionMode(QtWidgets
.QAbstractItemView
.ExtendedSelection
)
126 self
.headerItem().setHidden(True)
127 self
.setAllColumnsShowFocus(True)
128 self
.setSortingEnabled(False)
129 self
.setUniformRowHeights(True)
130 self
.setAnimated(True)
131 self
.setRootIsDecorated(False)
132 self
.setDragEnabled(True)
133 self
.setAutoScroll(False)
135 if not prefs
.status_indent(context
):
136 self
.setIndentation(0)
139 compare
= icons
.compare()
140 question
= icons
.question()
141 self
._add
_toplevel
_item
(N_('Staged'), ok
, hide
=True)
142 self
._add
_toplevel
_item
(N_('Unmerged'), compare
, hide
=True)
143 self
._add
_toplevel
_item
(N_('Modified'), compare
, hide
=True)
144 self
._add
_toplevel
_item
(N_('Untracked'), question
, hide
=True)
146 # Used to restore the selection
147 self
.old_vscroll
= None
148 self
.old_hscroll
= None
149 self
.old_selection
= None
150 self
.old_contents
= None
151 self
.old_current_item
= None
152 self
.previous_contents
= None
153 self
.was_visible
= True
154 self
.expanded_items
= set()
156 self
.image_formats
= qtutils
.ImageFormats()
158 self
.process_selection_action
= qtutils
.add_action(
160 cmds
.StageOrUnstage
.name(),
161 self
._stage
_selection
,
162 hotkeys
.STAGE_SELECTION
,
164 self
.process_selection_action
.setIcon(icons
.add())
166 self
.stage_or_unstage_all_action
= qtutils
.add_action(
168 cmds
.StageOrUnstageAll
.name(),
169 cmds
.run(cmds
.StageOrUnstageAll
, self
.context
),
172 self
.stage_or_unstage_all_action
.setIcon(icons
.add())
174 self
.revert_unstaged_edits_action
= qtutils
.add_action(
176 cmds
.RevertUnstagedEdits
.name(),
177 cmds
.run(cmds
.RevertUnstagedEdits
, context
),
180 self
.revert_unstaged_edits_action
.setIcon(icons
.undo())
182 self
.launch_difftool_action
= qtutils
.add_action(
184 cmds
.LaunchDifftool
.name(),
185 cmds
.run(cmds
.LaunchDifftool
, context
),
188 self
.launch_difftool_action
.setIcon(icons
.diff())
190 self
.launch_editor_action
= actions
.launch_editor_at_line(
191 context
, self
, *hotkeys
.ACCEPT
194 if not utils
.is_win32():
195 self
.default_app_action
= common
.default_app_action(
196 context
, self
, self
.selected_group
199 self
.parent_dir_action
= common
.parent_dir_action(
200 context
, self
, self
.selected_group
203 self
.terminal_action
= common
.terminal_action(
204 context
, self
, self
.selected_group
207 self
.up_action
= qtutils
.add_action(
212 hotkeys
.MOVE_UP_SECONDARY
,
215 self
.down_action
= qtutils
.add_action(
220 hotkeys
.MOVE_DOWN_SECONDARY
,
223 self
.copy_path_action
= qtutils
.add_action(
225 N_('Copy Path to Clipboard'),
226 partial(copy_path
, context
),
229 self
.copy_path_action
.setIcon(icons
.copy())
231 self
.copy_relpath_action
= qtutils
.add_action(
233 N_('Copy Relative Path to Clipboard'),
234 partial(copy_relpath
, context
),
237 self
.copy_relpath_action
.setIcon(icons
.copy())
239 self
.copy_leading_path_action
= qtutils
.add_action(
241 N_('Copy Leading Path to Clipboard'),
242 partial(copy_leading_path
, context
),
244 self
.copy_leading_path_action
.setIcon(icons
.copy())
246 self
.copy_basename_action
= qtutils
.add_action(
247 self
, N_('Copy Basename to Clipboard'), partial(copy_basename
, context
)
249 self
.copy_basename_action
.setIcon(icons
.copy())
251 self
.copy_customize_action
= qtutils
.add_action(
252 self
, N_('Customize...'), partial(customize_copy_actions
, context
, self
)
254 self
.copy_customize_action
.setIcon(icons
.configure())
256 self
.view_history_action
= qtutils
.add_action(
257 self
, N_('View History...'), partial(view_history
, context
), hotkeys
.HISTORY
260 self
.view_blame_action
= qtutils
.add_action(
261 self
, N_('Blame...'), partial(view_blame
, context
), hotkeys
.BLAME
264 self
.annex_add_action
= qtutils
.add_action(
265 self
, N_('Add to Git Annex'), cmds
.run(cmds
.AnnexAdd
, context
)
268 self
.lfs_track_action
= qtutils
.add_action(
269 self
, N_('Add to Git LFS'), cmds
.run(cmds
.LFSTrack
, context
)
272 # MoveToTrash and Delete use the same shortcut.
273 # We will only bind one of them, depending on whether or not the
274 # MoveToTrash command is available. When available, the hotkey
275 # is bound to MoveToTrash, otherwise it is bound to Delete.
276 if cmds
.MoveToTrash
.AVAILABLE
:
277 self
.move_to_trash_action
= qtutils
.add_action(
279 N_('Move files to trash'),
280 self
._trash
_untracked
_files
,
283 self
.move_to_trash_action
.setIcon(icons
.discard())
284 delete_shortcut
= hotkeys
.DELETE_FILE
286 self
.move_to_trash_action
= None
287 delete_shortcut
= hotkeys
.DELETE_FILE_SECONDARY
289 self
.delete_untracked_files_action
= qtutils
.add_action(
290 self
, N_('Delete Files...'), self
._delete
_untracked
_files
, delete_shortcut
292 self
.delete_untracked_files_action
.setIcon(icons
.discard())
294 self
.about_to_update
.connect(self
._about
_to
_update
, type=Qt
.QueuedConnection
)
295 self
.set_previous_contents
.connect(
296 self
._set
_previous
_contents
, type=Qt
.QueuedConnection
)
297 self
.updated
.connect(self
.refresh
, type=Qt
.QueuedConnection
)
298 self
.diff_text_changed
.connect(
299 self
._make
_current
_item
_visible
, type=Qt
.QueuedConnection
302 # The model is stored as self.m because self.model() is a
303 # QTreeWidgetItem method that returns a QAbstractItemModel.
304 self
.m
= context
.model
305 # Forward the previous_contents notification through self.set_previous_contents.
307 self
.m
.message_previous_contents
, self
.set_previous_contents
.emit
309 # Forward the about_to_update notification through self.about_to_udpate.
310 self
.m
.add_observer(self
.m
.message_about_to_update
, self
.about_to_update
.emit
)
311 # Foward the updated notification through self.updated.
312 self
.m
.add_observer(self
.m
.message_updated
, self
.updated
.emit
)
314 self
.m
.message_diff_text_changed
, self
.diff_text_changed
.emit
316 # pylint: disable=no-member
317 self
.itemSelectionChanged
.connect(self
.show_selection
)
318 self
.itemDoubleClicked
.connect(cmds
.run(cmds
.StageOrUnstage
, self
.context
))
319 self
.itemCollapsed
.connect(lambda x
: self
._update
_column
_widths
())
320 self
.itemExpanded
.connect(lambda x
: self
._update
_column
_widths
())
322 def _make_current_item_visible(self
):
323 item
= self
.currentItem()
325 qtutils
.scroll_to_item(self
, item
)
327 def _add_toplevel_item(self
, txt
, icon
, hide
=False):
328 context
= self
.context
330 if prefs
.bold_headers(context
):
335 item
= QtWidgets
.QTreeWidgetItem(self
)
336 item
.setFont(0, font
)
338 item
.setIcon(0, icon
)
339 if prefs
.bold_headers(context
):
340 item
.setBackground(0, self
.palette().midlight())
344 def _restore_selection(self
):
345 """Apply the old selection to the newly updated items"""
346 # This function is called after a new set of items have been added to
347 # the per-category file lists. Its purpose is to either restore the
348 # existing selection or to create a new intuitive selection based on
349 # a combination of the old items, the old selection and the new items.
350 if not self
.old_selection
or not self
.old_contents
:
352 # The old set of categorized files.
353 old_c
= self
.old_contents
355 old_s
= self
.old_selection
356 # The current/new set of categorized files.
357 new_c
= self
.contents()
359 select_staged
= partial(
360 _select_item
, self
, new_c
.staged
, self
._staged
_item
362 select_unmerged
= partial(
363 _select_item
, self
, new_c
.unmerged
, self
._unmerged
_item
365 select_modified
= partial(
366 _select_item
, self
, new_c
.modified
, self
._modified
_item
368 select_untracked
= partial(
369 _select_item
, self
, new_c
.untracked
, self
._untracked
_item
373 (set(new_c
.staged
), old_c
.staged
, set(old_s
.staged
), select_staged
),
374 (set(new_c
.unmerged
), old_c
.unmerged
, set(old_s
.unmerged
), select_unmerged
),
375 (set(new_c
.modified
), old_c
.modified
, set(old_s
.modified
), select_modified
),
377 set(new_c
.untracked
),
379 set(old_s
.untracked
),
384 # Restore the current item
385 if self
.old_current_item
:
386 category
, idx
= self
.old_current_item
387 if _apply_toplevel_selection(self
, category
, idx
):
389 # Reselect the current item
390 selection_info
= saved_selection
[category
]
391 new
= selection_info
[NEW_PATHS_IDX
]
392 old
= selection_info
[OLD_PATHS_IDX
]
393 reselect
= selection_info
[SELECT_FN_IDX
]
398 if item
and item
in new
:
399 reselect(item
, current
=True)
401 # Restore previously selected items.
402 # When reselecting in this section we only care that the items are
403 # selected; we do not need to rerun the callbacks which were triggered
404 # above for the current item. Block signals to skip the callbacks.
406 # Reselect items that were previously selected and still exist in the
407 # current path lists. This handles a common case such as a Ctrl-R
408 # refresh which results in the same exact path state.
411 with qtutils
.BlockSignals(self
):
412 for (new
, old
, sel
, reselect
) in saved_selection
:
415 reselect(item
, current
=False)
418 # The status widget is used to interactively work your way down the
419 # list of Staged, Unmerged, Modified and Untracked items and perform
420 # an operation on them.
422 # For Staged items we intend to work our way down the list of Staged
423 # items while we unstage each item. For every other category we work
424 # our way down the list of {Unmerged,Modified,Untracked} items while
425 # we stage each item.
427 # The following block of code implements the behavior of selecting
428 # the next item based on the previous selection.
429 for (new
, old
, sel
, reselect
) in saved_selection
:
430 # When modified is staged, select the next modified item
431 # When unmerged is staged, select the next unmerged item
432 # When unstaging, select the next staged item
433 # When staging untracked files, select the next untracked item
434 if len(new
) >= len(old
):
435 # The list did not shrink so it is not one of these cases.
438 # The item still exists so ignore it
439 if item
in new
or item
not in old
:
441 # The item no longer exists in this list so search for
442 # its nearest neighbors and select them instead.
443 idx
= old
.index(item
)
444 for j
in itertools
.chain(old
[idx
+ 1 :], reversed(old
[:idx
])):
446 reselect(j
, current
=True)
449 # If we already reselected stuff then there's nothing more to do.
452 # If we got this far then nothing was reselected and made current.
453 # Try a few more heuristics that we can use to keep something selected.
454 if self
.old_current_item
:
455 category
, idx
= self
.old_current_item
456 _transplant_selection_across_sections(
457 category
, idx
, self
.previous_contents
, saved_selection
460 def _restore_scrollbars(self
):
461 """Restore scrollbars to the stored values"""
462 qtutils
.set_scrollbar_values(self
, self
.old_hscroll
, self
.old_vscroll
)
463 self
.old_hscroll
= None
464 self
.old_vscroll
= None
466 def _stage_selection(self
):
467 """Stage or unstage files according to the selection"""
468 context
= self
.context
469 selected_indexes
= self
.selected_indexes()
471 category
== HEADER_IDX
for (category
, idx
) in selected_indexes
475 idx
== STAGED_IDX
and category
== HEADER_IDX
476 for (category
, idx
) in selected_indexes
479 idx
== MODIFIED_IDX
and category
== HEADER_IDX
480 for (category
, idx
) in selected_indexes
483 idx
== UNTRACKED_IDX
and category
== HEADER_IDX
484 for (category
, idx
) in selected_indexes
486 # A header item: 'Staged', 'Modified' or 'Untracked'.
488 # If we have the staged header selected then the only sensible
489 # thing to do is to unstage everything and nothing else, even
490 # if the modified or untracked headers are selected.
491 cmds
.do(cmds
.UnstageAll
, context
)
492 return # Everything was unstaged. There's nothing more to be done.
493 elif is_modified
and is_untracked
:
494 # If both modified and untracked headers are selected then
496 cmds
.do(cmds
.StageModifiedAndUntracked
, context
)
497 return # Nothing more to do.
498 # At this point we may stage all modified and untracked, and then
499 # possibly a subset of the other category (eg. all modified and
500 # some untracked). We don't return here so that StageOrUnstage
501 # gets a chance to run below.
503 cmds
.do(cmds
.StageModified
, context
)
505 cmds
.do(cmds
.StageUntracked
, context
)
507 # Do nothing for unmerged items, by design
509 # Now handle individual files
510 cmds
.do(cmds
.StageOrUnstage
, context
)
512 def _staged_item(self
, itemidx
):
513 return self
._subtree
_item
(STAGED_IDX
, itemidx
)
515 def _modified_item(self
, itemidx
):
516 return self
._subtree
_item
(MODIFIED_IDX
, itemidx
)
518 def _unmerged_item(self
, itemidx
):
519 return self
._subtree
_item
(UNMERGED_IDX
, itemidx
)
521 def _untracked_item(self
, itemidx
):
522 return self
._subtree
_item
(UNTRACKED_IDX
, itemidx
)
524 def _unstaged_item(self
, itemidx
):
526 item
= self
.topLevelItem(MODIFIED_IDX
)
527 count
= item
.childCount()
529 return item
.child(itemidx
)
531 item
= self
.topLevelItem(UNMERGED_IDX
)
532 count
+= item
.childCount()
534 return item
.child(itemidx
)
536 item
= self
.topLevelItem(UNTRACKED_IDX
)
537 count
+= item
.childCount()
539 return item
.child(itemidx
)
543 def _subtree_item(self
, idx
, itemidx
):
544 parent
= self
.topLevelItem(idx
)
545 return parent
.child(itemidx
)
547 def _set_previous_contents(self
, staged
, unmerged
, modified
, untracked
):
548 """Callback triggered right before the model changes its contents"""
549 self
.previous_contents
= selection
.State(staged
, unmerged
, modified
, untracked
)
551 def _about_to_update(self
):
552 self
._save
_scrollbars
()
553 self
._save
_selection
()
555 def _save_scrollbars(self
):
556 """Store the scrollbar values for later application"""
557 hscroll
, vscroll
= qtutils
.get_scrollbar_values(self
)
558 if hscroll
is not None:
559 self
.old_hscroll
= hscroll
560 if vscroll
is not None:
561 self
.old_vscroll
= vscroll
563 def current_item(self
):
564 s
= self
.selected_indexes()
567 current
= self
.currentItem()
570 idx
= self
.indexFromItem(current
)
571 if idx
.parent().isValid():
572 parent_idx
= idx
.parent()
573 entry
= (parent_idx
.row(), idx
.row())
575 entry
= (HEADER_IDX
, idx
.row())
578 def _save_selection(self
):
579 self
.old_contents
= self
.contents()
580 self
.old_selection
= self
.selection()
581 self
.old_current_item
= self
.current_item()
584 self
._set
_staged
(self
.m
.staged
)
585 self
._set
_modified
(self
.m
.modified
)
586 self
._set
_unmerged
(self
.m
.unmerged
)
587 self
._set
_untracked
(self
.m
.untracked
)
588 self
._update
_column
_widths
()
589 self
._update
_actions
()
590 self
._restore
_selection
()
591 self
._restore
_scrollbars
()
593 def _update_actions(self
, selected
=None):
595 selected
= self
.selection_model
.selection()
596 can_revert_edits
= bool(selected
.staged
or selected
.modified
)
597 self
.revert_unstaged_edits_action
.setEnabled(can_revert_edits
)
599 def _set_staged(self
, items
):
600 """Adds items to the 'Staged' subtree."""
601 with qtutils
.BlockSignals(self
):
607 deleted_set
=self
.m
.staged_deleted
,
610 def _set_modified(self
, items
):
611 """Adds items to the 'Modified' subtree."""
612 with qtutils
.BlockSignals(self
):
617 deleted_set
=self
.m
.unstaged_deleted
,
620 def _set_unmerged(self
, items
):
621 """Adds items to the 'Unmerged' subtree."""
622 deleted_set
= set([path
for path
in items
if not core
.exists(path
)])
623 with qtutils
.BlockSignals(self
):
625 items
, UNMERGED_IDX
, N_('Unmerged'), deleted_set
=deleted_set
628 def _set_untracked(self
, items
):
629 """Adds items to the 'Untracked' subtree."""
630 with qtutils
.BlockSignals(self
):
632 items
, UNTRACKED_IDX
, N_('Untracked'), untracked
=True
636 self
, items
, idx
, parent_title
, staged
=False, untracked
=False, deleted_set
=None
638 """Add a list of items to a treewidget item."""
639 parent
= self
.topLevelItem(idx
)
640 hide
= not bool(items
)
641 parent
.setHidden(hide
)
643 # sip v4.14.7 and below leak memory in parent.takeChildren()
644 # so we use this backwards-compatible construct instead
645 while parent
.takeChild(0) is not None:
649 deleted
= deleted_set
is not None and item
in deleted_set
650 treeitem
= qtutils
.create_treeitem(
651 item
, staged
=staged
, deleted
=deleted
, untracked
=untracked
653 parent
.addChild(treeitem
)
654 self
._expand
_items
(idx
, items
)
656 if prefs
.status_show_totals(self
.context
):
657 parent
.setText(0, '%s (%s)' % (parent_title
, len(items
)))
659 def _update_column_widths(self
):
660 self
.resizeColumnToContents(0)
662 def _expand_items(self
, idx
, items
):
663 """Expand the top-level category "folder" once and only once."""
664 # Don't do this if items is empty; this makes it so that we
665 # don't add the top-level index into the expanded_items set
666 # until an item appears in a particular category.
669 # Only run this once; we don't want to re-expand items that
670 # we've clicked on to re-collapse on updated().
671 if idx
in self
.expanded_items
:
673 self
.expanded_items
.add(idx
)
674 item
= self
.topLevelItem(idx
)
676 self
.expandItem(item
)
678 def contextMenuEvent(self
, event
):
679 """Create context menus for the repo status tree."""
680 menu
= self
._create
_context
_menu
()
681 menu
.exec_(self
.mapToGlobal(event
.pos()))
683 def _create_context_menu(self
):
684 """Set up the status menu for the repo status tree."""
686 menu
= qtutils
.create_menu('Status', self
)
687 selected_indexes
= self
.selected_indexes()
689 category
, idx
= selected_indexes
[0]
690 # A header item e.g. 'Staged', 'Modified', etc.
691 if category
== HEADER_IDX
:
692 return self
._create
_header
_context
_menu
(menu
, idx
)
695 self
._create
_staged
_context
_menu
(menu
, s
)
697 self
._create
_unmerged
_context
_menu
(menu
, s
)
699 self
._create
_unstaged
_context
_menu
(menu
, s
)
701 if not utils
.is_win32():
702 if not menu
.isEmpty():
704 if not self
.selection_model
.is_empty():
705 menu
.addAction(self
.default_app_action
)
706 menu
.addAction(self
.parent_dir_action
)
708 if self
.terminal_action
is not None:
709 menu
.addAction(self
.terminal_action
)
711 self
._add
_copy
_actions
(menu
)
715 def _add_copy_actions(self
, menu
):
716 """Add the "Copy" sub-menu"""
717 enabled
= self
.selection_model
.filename() is not None
718 self
.copy_path_action
.setEnabled(enabled
)
719 self
.copy_relpath_action
.setEnabled(enabled
)
720 self
.copy_leading_path_action
.setEnabled(enabled
)
721 self
.copy_basename_action
.setEnabled(enabled
)
722 copy_icon
= icons
.copy()
725 copy_menu
= QtWidgets
.QMenu(N_('Copy...'), menu
)
726 menu
.addMenu(copy_menu
)
728 copy_menu
.setIcon(copy_icon
)
729 copy_menu
.addAction(self
.copy_path_action
)
730 copy_menu
.addAction(self
.copy_relpath_action
)
731 copy_menu
.addAction(self
.copy_leading_path_action
)
732 copy_menu
.addAction(self
.copy_basename_action
)
734 settings
= Settings
.read()
735 copy_formats
= settings
.copy_formats
737 copy_menu
.addSeparator()
739 context
= self
.context
740 for entry
in copy_formats
:
741 name
= entry
.get('name', '')
742 fmt
= entry
.get('format', '')
744 action
= copy_menu
.addAction(name
, partial(copy_format
, context
, fmt
))
745 action
.setIcon(copy_icon
)
746 action
.setEnabled(enabled
)
748 copy_menu
.addSeparator()
749 copy_menu
.addAction(self
.copy_customize_action
)
751 def _create_header_context_menu(self
, menu
, idx
):
752 context
= self
.context
753 if idx
== STAGED_IDX
:
755 icons
.remove(), N_('Unstage All'), cmds
.run(cmds
.UnstageAll
, context
)
757 elif idx
== UNMERGED_IDX
:
758 action
= menu
.addAction(
760 cmds
.StageUnmerged
.name(),
761 cmds
.run(cmds
.StageUnmerged
, context
),
763 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
764 elif idx
== MODIFIED_IDX
:
765 action
= menu
.addAction(
767 cmds
.StageModified
.name(),
768 cmds
.run(cmds
.StageModified
, context
),
770 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
771 elif idx
== UNTRACKED_IDX
:
772 action
= menu
.addAction(
774 cmds
.StageUntracked
.name(),
775 cmds
.run(cmds
.StageUntracked
, context
),
777 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
780 def _create_staged_context_menu(self
, menu
, s
):
781 if s
.staged
[0] in self
.m
.submodules
:
782 return self
._create
_staged
_submodule
_context
_menu
(menu
, s
)
784 context
= self
.context
785 if self
.m
.unstageable():
786 action
= menu
.addAction(
788 N_('Unstage Selected'),
789 cmds
.run(cmds
.Unstage
, context
, self
.staged()),
791 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
793 menu
.addAction(self
.launch_editor_action
)
795 # Do all of the selected items exist?
797 i
not in self
.m
.staged_deleted
and core
.exists(i
) for i
in self
.staged()
801 menu
.addAction(self
.launch_difftool_action
)
803 if self
.m
.undoable():
804 menu
.addAction(self
.revert_unstaged_edits_action
)
806 menu
.addAction(self
.view_history_action
)
807 menu
.addAction(self
.view_blame_action
)
810 def _create_staged_submodule_context_menu(self
, menu
, s
):
811 context
= self
.context
812 path
= core
.abspath(s
.staged
[0])
813 if len(self
.staged()) == 1:
816 N_('Launch git-cola'),
817 cmds
.run(cmds
.OpenRepo
, context
, path
),
820 action
= menu
.addAction(
822 N_('Unstage Selected'),
823 cmds
.run(cmds
.Unstage
, context
, self
.staged()),
825 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
827 menu
.addAction(self
.view_history_action
)
830 def _create_unmerged_context_menu(self
, menu
, _s
):
831 context
= self
.context
832 menu
.addAction(self
.launch_difftool_action
)
834 action
= menu
.addAction(
836 N_('Stage Selected'),
837 cmds
.run(cmds
.Stage
, context
, self
.unstaged()),
839 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
841 menu
.addAction(self
.launch_editor_action
)
842 menu
.addAction(self
.view_history_action
)
843 menu
.addAction(self
.view_blame_action
)
846 def _create_unstaged_context_menu(self
, menu
, s
):
847 context
= self
.context
848 modified_submodule
= s
.modified
and s
.modified
[0] in self
.m
.submodules
849 if modified_submodule
:
850 return self
._create
_modified
_submodule
_context
_menu
(menu
, s
)
852 if self
.m
.stageable():
853 action
= menu
.addAction(
855 N_('Stage Selected'),
856 cmds
.run(cmds
.Stage
, context
, self
.unstaged()),
858 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
860 if not self
.selection_model
.is_empty():
861 menu
.addAction(self
.launch_editor_action
)
863 # Do all of the selected items exist?
865 i
not in self
.m
.unstaged_deleted
and core
.exists(i
) for i
in self
.staged()
868 if all_exist
and s
.modified
and self
.m
.stageable():
869 menu
.addAction(self
.launch_difftool_action
)
871 if s
.modified
and self
.m
.stageable():
872 if self
.m
.undoable():
874 menu
.addAction(self
.revert_unstaged_edits_action
)
876 if all_exist
and s
.untracked
:
877 # Git Annex / Git LFS
879 lfs
= core
.find_executable('git-lfs')
883 menu
.addAction(self
.annex_add_action
)
885 menu
.addAction(self
.lfs_track_action
)
888 if self
.move_to_trash_action
is not None:
889 menu
.addAction(self
.move_to_trash_action
)
890 menu
.addAction(self
.delete_untracked_files_action
)
895 partial(gitignore
.gitignore_view
, self
.context
),
898 if not self
.selection_model
.is_empty():
899 menu
.addAction(self
.view_history_action
)
900 menu
.addAction(self
.view_blame_action
)
903 def _create_modified_submodule_context_menu(self
, menu
, s
):
904 context
= self
.context
905 path
= core
.abspath(s
.modified
[0])
906 if len(self
.unstaged()) == 1:
909 N_('Launch git-cola'),
910 cmds
.run(cmds
.OpenRepo
, context
, path
),
914 N_('Update this submodule'),
915 cmds
.run(cmds
.SubmoduleUpdate
, context
, path
),
919 if self
.m
.stageable():
921 action
= menu
.addAction(
923 N_('Stage Selected'),
924 cmds
.run(cmds
.Stage
, context
, self
.unstaged()),
926 action
.setShortcut(hotkeys
.STAGE_SELECTION
)
928 menu
.addAction(self
.view_history_action
)
931 def _delete_untracked_files(self
):
932 cmds
.do(cmds
.Delete
, self
.context
, self
.untracked())
934 def _trash_untracked_files(self
):
935 cmds
.do(cmds
.MoveToTrash
, self
.context
, self
.untracked())
937 def selected_path(self
):
938 s
= self
.single_selection()
939 return s
.staged
or s
.unmerged
or s
.modified
or s
.untracked
or None
941 def single_selection(self
):
942 """Scan across staged, modified, etc. and return a single item."""
952 unmerged
= s
.unmerged
[0]
954 modified
= s
.modified
[0]
956 untracked
= s
.untracked
[0]
958 return selection
.State(staged
, unmerged
, modified
, untracked
)
960 def selected_indexes(self
):
961 """Returns a list of (category, row) representing the tree selection."""
962 selected
= self
.selectedIndexes()
965 if idx
.parent().isValid():
966 parent_idx
= idx
.parent()
967 entry
= (parent_idx
.row(), idx
.row())
969 entry
= (HEADER_IDX
, idx
.row())
974 """Return the current selection in the repo status tree."""
975 return selection
.State(
976 self
.staged(), self
.unmerged(), self
.modified(), self
.untracked()
980 """Return all of the current files in a selection.State container"""
981 return selection
.State(
982 self
.m
.staged
, self
.m
.unmerged
, self
.m
.modified
, self
.m
.untracked
986 """Return all of the current active files as a flast list"""
988 return c
.staged
+ c
.unmerged
+ c
.modified
+ c
.untracked
990 def selected_group(self
):
991 """A list of selected files in various states of being"""
992 return selection
.pick(self
.selection())
994 def selected_idx(self
):
996 s
= self
.single_selection()
998 for content
, sel
in zip(c
, s
):
1002 return offset
+ content
.index(sel
)
1003 offset
+= len(content
)
1006 def select_by_index(self
, idx
):
1009 (c
.staged
, STAGED_IDX
),
1010 (c
.unmerged
, UNMERGED_IDX
),
1011 (c
.modified
, MODIFIED_IDX
),
1012 (c
.untracked
, UNTRACKED_IDX
),
1014 for content
, toplevel_idx
in to_try
:
1017 if idx
< len(content
):
1018 parent
= self
.topLevelItem(toplevel_idx
)
1019 item
= parent
.child(idx
)
1020 if item
is not None:
1021 qtutils
.select_item(self
, item
)
1026 return qtutils
.get_selected_values(self
, STAGED_IDX
, self
.m
.staged
)
1029 return self
.unmerged() + self
.modified() + self
.untracked()
1032 return qtutils
.get_selected_values(self
, MODIFIED_IDX
, self
.m
.modified
)
1035 return qtutils
.get_selected_values(self
, UNMERGED_IDX
, self
.m
.unmerged
)
1037 def untracked(self
):
1038 return qtutils
.get_selected_values(self
, UNTRACKED_IDX
, self
.m
.untracked
)
1040 def staged_items(self
):
1041 return qtutils
.get_selected_items(self
, STAGED_IDX
)
1043 def unstaged_items(self
):
1044 return self
.unmerged_items() + self
.modified_items() + self
.untracked_items()
1046 def modified_items(self
):
1047 return qtutils
.get_selected_items(self
, MODIFIED_IDX
)
1049 def unmerged_items(self
):
1050 return qtutils
.get_selected_items(self
, UNMERGED_IDX
)
1052 def untracked_items(self
):
1053 return qtutils
.get_selected_items(self
, UNTRACKED_IDX
)
1055 def show_selection(self
):
1056 """Show the selected item."""
1057 context
= self
.context
1058 qtutils
.scroll_to_item(self
, self
.currentItem())
1059 # Sync the selection model
1060 selected
= self
.selection()
1061 selection_model
= self
.selection_model
1062 selection_model
.set_selection(selected
)
1063 self
._update
_actions
(selected
=selected
)
1065 selected_indexes
= self
.selected_indexes()
1066 if not selected_indexes
:
1067 if self
.m
.amending():
1068 cmds
.do(cmds
.SetDiffText
, context
, '')
1070 cmds
.do(cmds
.ResetMode
, context
)
1073 # A header item e.g. 'Staged', 'Modified', etc.
1074 category
, idx
= selected_indexes
[0]
1075 header
= category
== HEADER_IDX
1078 STAGED_IDX
: cmds
.DiffStagedSummary
,
1079 MODIFIED_IDX
: cmds
.Diffstat
,
1080 # TODO implement UnmergedSummary
1081 # UNMERGED_IDX: cmds.UnmergedSummary,
1082 UNTRACKED_IDX
: cmds
.UntrackedSummary
,
1083 }.get(idx
, cmds
.Diffstat
)
1084 cmds
.do(cls
, context
)
1087 staged
= category
== STAGED_IDX
1088 modified
= category
== MODIFIED_IDX
1089 unmerged
= category
== UNMERGED_IDX
1090 untracked
= category
== UNTRACKED_IDX
1093 item
= self
.staged_items()[0]
1095 item
= self
.unmerged_items()[0]
1097 item
= self
.modified_items()[0]
1099 item
= self
.unstaged_items()[0]
1101 item
= None # this shouldn't happen
1102 assert item
is not None
1105 deleted
= item
.deleted
1106 image
= self
.image_formats
.ok(path
)
1108 # Update the diff text
1110 cmds
.do(cmds
.DiffStaged
, context
, path
, deleted
=deleted
)
1112 cmds
.do(cmds
.Diff
, context
, path
, deleted
=deleted
)
1114 cmds
.do(cmds
.Diff
, context
, path
)
1116 cmds
.do(cmds
.ShowUntracked
, context
, path
)
1118 # Images are diffed differently.
1119 # DiffImage transitions the diff mode to image.
1120 # DiffText transitions the diff mode to text.
1133 cmds
.do(cmds
.DiffText
, context
)
1135 def select_header(self
):
1136 """Select an active header, which triggers a diffstat"""
1143 item
= self
.topLevelItem(idx
)
1144 if item
.childCount() > 0:
1145 self
.clearSelection()
1146 self
.setCurrentItem(item
)
1150 idx
= self
.selected_idx()
1151 all_files
= self
.all_files()
1153 selected_indexes
= self
.selected_indexes()
1154 if selected_indexes
:
1155 category
, toplevel_idx
= selected_indexes
[0]
1156 if category
== HEADER_IDX
:
1157 item
= self
.itemAbove(self
.topLevelItem(toplevel_idx
))
1158 if item
is not None:
1159 qtutils
.select_item(self
, item
)
1162 self
.select_by_index(len(all_files
) - 1)
1165 self
.select_by_index(idx
- 1)
1167 self
.select_by_index(len(all_files
) - 1)
1169 def move_down(self
):
1170 idx
= self
.selected_idx()
1171 all_files
= self
.all_files()
1173 selected_indexes
= self
.selected_indexes()
1174 if selected_indexes
:
1175 category
, toplevel_idx
= selected_indexes
[0]
1176 if category
== HEADER_IDX
:
1177 item
= self
.itemBelow(self
.topLevelItem(toplevel_idx
))
1178 if item
is not None:
1179 qtutils
.select_item(self
, item
)
1182 self
.select_by_index(0)
1184 if idx
+ 1 < len(all_files
):
1185 self
.select_by_index(idx
+ 1)
1187 self
.select_by_index(0)
1189 def mimeData(self
, items
):
1190 """Return a list of absolute-path URLs"""
1191 context
= self
.context
1192 paths
= qtutils
.paths_from_items(items
, item_filter
=_item_filter
)
1193 return qtutils
.mimedata_from_paths(context
, paths
)
1195 # pylint: disable=no-self-use
1196 def mimeTypes(self
):
1197 return qtutils
.path_mimetypes()
1200 def _item_filter(item
):
1201 return not item
.deleted
and core
.exists(item
.path
)
1204 def view_blame(context
):
1205 """Signal that we should view blame for paths."""
1206 cmds
.do(cmds
.BlamePaths
, context
)
1209 def view_history(context
):
1210 """Signal that we should view history for paths."""
1211 cmds
.do(cmds
.VisualizePaths
, context
, context
.selection
.union())
1214 def copy_path(context
, absolute
=True):
1215 """Copy a selected path to the clipboard"""
1216 filename
= context
.selection
.filename()
1217 qtutils
.copy_path(filename
, absolute
=absolute
)
1220 def copy_relpath(context
):
1221 """Copy a selected relative path to the clipboard"""
1222 copy_path(context
, absolute
=False)
1225 def copy_basename(context
):
1226 filename
= os
.path
.basename(context
.selection
.filename())
1227 basename
, _
= os
.path
.splitext(filename
)
1228 qtutils
.copy_path(basename
, absolute
=False)
1231 def copy_leading_path(context
):
1232 """Copy the selected leading path to the clipboard"""
1233 filename
= context
.selection
.filename()
1234 dirname
= os
.path
.dirname(filename
)
1235 qtutils
.copy_path(dirname
, absolute
=False)
1238 def copy_format(context
, fmt
):
1240 values
['path'] = path
= context
.selection
.filename()
1241 values
['abspath'] = abspath
= os
.path
.abspath(path
)
1242 values
['absdirname'] = os
.path
.dirname(abspath
)
1243 values
['dirname'] = os
.path
.dirname(path
)
1244 values
['filename'] = os
.path
.basename(path
)
1245 values
['basename'], values
['ext'] = os
.path
.splitext(os
.path
.basename(path
))
1246 qtutils
.set_clipboard(fmt
% values
)
1249 def show_help(context
):
1252 Format String Variables
1253 -----------------------
1254 %(path)s = relative file path
1255 %(abspath)s = absolute file path
1256 %(dirname)s = relative directory path
1257 %(absdirname)s = absolute directory path
1258 %(filename)s = file basename
1259 %(basename)s = file basename without extension
1260 %(ext)s = file extension
1263 title
= N_('Help - Custom Copy Actions')
1264 return text
.text_dialog(context
, help_text
, title
)
1267 class StatusFilterWidget(QtWidgets
.QWidget
):
1268 def __init__(self
, context
, parent
=None):
1269 QtWidgets
.QWidget
.__init
__(self
, parent
)
1270 self
.context
= context
1272 hint
= N_('Filter paths...')
1273 self
.text
= completion
.GitStatusFilterLineEdit(context
, hint
=hint
, parent
=self
)
1274 self
.text
.setToolTip(hint
)
1275 self
.setFocusProxy(self
.text
)
1278 self
.main_layout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
, self
.text
)
1279 self
.setLayout(self
.main_layout
)
1282 # pylint: disable=no-member
1283 widget
.changed
.connect(self
.apply_filter
)
1284 widget
.cleared
.connect(self
.apply_filter
)
1285 widget
.enter
.connect(self
.apply_filter
)
1286 widget
.editingFinished
.connect(self
.apply_filter
)
1288 def apply_filter(self
):
1289 value
= get(self
.text
)
1290 if value
== self
._filter
:
1292 self
._filter
= value
1293 paths
= utils
.shell_split(value
)
1294 self
.context
.model
.update_path_filter(paths
)
1297 def customize_copy_actions(context
, parent
):
1298 """Customize copy actions"""
1299 dialog
= CustomizeCopyActions(context
, parent
)
1304 class CustomizeCopyActions(standard
.Dialog
):
1305 def __init__(self
, context
, parent
):
1306 standard
.Dialog
.__init
__(self
, parent
=parent
)
1307 self
.setWindowTitle(N_('Custom Copy Actions'))
1309 self
.context
= context
1310 self
.table
= QtWidgets
.QTableWidget(self
)
1311 self
.table
.setColumnCount(2)
1312 self
.table
.setHorizontalHeaderLabels([N_('Action Name'), N_('Format String')])
1313 self
.table
.setSortingEnabled(False)
1314 self
.table
.verticalHeader().hide()
1315 self
.table
.horizontalHeader().setStretchLastSection(True)
1317 self
.add_button
= qtutils
.create_button(N_('Add'))
1318 self
.remove_button
= qtutils
.create_button(N_('Remove'))
1319 self
.remove_button
.setEnabled(False)
1320 self
.show_help_button
= qtutils
.create_button(N_('Show Help'))
1321 self
.show_help_button
.setShortcut(hotkeys
.QUESTION
)
1323 self
.close_button
= qtutils
.close_button()
1324 self
.save_button
= qtutils
.ok_button(N_('Save'))
1326 self
.buttons
= qtutils
.hbox(
1328 defs
.button_spacing
,
1331 self
.show_help_button
,
1337 layout
= qtutils
.vbox(defs
.margin
, defs
.spacing
, self
.table
, self
.buttons
)
1338 self
.setLayout(layout
)
1340 qtutils
.connect_button(self
.add_button
, self
.add
)
1341 qtutils
.connect_button(self
.remove_button
, self
.remove
)
1342 qtutils
.connect_button(self
.show_help_button
, partial(show_help
, context
))
1343 qtutils
.connect_button(self
.close_button
, self
.reject
)
1344 qtutils
.connect_button(self
.save_button
, self
.save
)
1345 qtutils
.add_close_action(self
)
1346 # pylint: disable=no-member
1347 self
.table
.itemSelectionChanged
.connect(self
.table_selection_changed
)
1349 self
.init_size(parent
=parent
)
1351 QtCore
.QTimer
.singleShot(0, self
.reload_settings
)
1353 def reload_settings(self
):
1354 # Called once after the GUI is initialized
1355 settings
= self
.context
.settings
1358 for entry
in settings
.copy_formats
:
1359 name_string
= entry
.get('name', '')
1360 format_string
= entry
.get('format', '')
1361 if name_string
and format_string
:
1362 name
= QtWidgets
.QTableWidgetItem(name_string
)
1363 fmt
= QtWidgets
.QTableWidgetItem(format_string
)
1364 rows
= table
.rowCount()
1365 table
.setRowCount(rows
+ 1)
1366 table
.setItem(rows
, 0, name
)
1367 table
.setItem(rows
, 1, fmt
)
1369 def export_state(self
):
1370 state
= super(CustomizeCopyActions
, self
).export_state()
1371 standard
.export_header_columns(self
.table
, state
)
1374 def apply_state(self
, state
):
1375 result
= super(CustomizeCopyActions
, self
).apply_state(state
)
1376 standard
.apply_header_columns(self
.table
, state
)
1380 self
.table
.setFocus()
1381 rows
= self
.table
.rowCount()
1382 self
.table
.setRowCount(rows
+ 1)
1384 name
= QtWidgets
.QTableWidgetItem(N_('Name'))
1385 fmt
= QtWidgets
.QTableWidgetItem(r
'%(path)s')
1386 self
.table
.setItem(rows
, 0, name
)
1387 self
.table
.setItem(rows
, 1, fmt
)
1389 self
.table
.setCurrentCell(rows
, 0)
1390 self
.table
.editItem(name
)
1393 """Remove selected items"""
1394 # Gather a unique set of rows and remove them in reverse order
1396 items
= self
.table
.selectedItems()
1398 rows
.add(self
.table
.row(item
))
1400 for row
in reversed(sorted(rows
)):
1401 self
.table
.removeRow(row
)
1405 for row
in range(self
.table
.rowCount()):
1406 name
= self
.table
.item(row
, 0)
1407 fmt
= self
.table
.item(row
, 1)
1410 'name': name
.text(),
1411 'format': fmt
.text(),
1413 copy_formats
.append(entry
)
1415 settings
= self
.context
.settings
1416 while settings
.copy_formats
:
1417 settings
.copy_formats
.pop()
1419 settings
.copy_formats
.extend(copy_formats
)
1424 def table_selection_changed(self
):
1425 items
= self
.table
.selectedItems()
1426 self
.remove_button
.setEnabled(bool(items
))
1429 def _select_item(widget
, path_list
, widget_getter
, item
, current
=False):
1430 """Select the widget item based on the list index"""
1431 # The path lists and widget indexes have a 1:1 correspondence.
1432 # Lookup the item filename in the list and use that index to
1433 # retrieve the widget item and select it.
1434 idx
= path_list
.index(item
)
1435 item
= widget_getter(idx
)
1437 widget
.setCurrentItem(item
)
1438 item
.setSelected(True)
1441 def _apply_toplevel_selection(widget
, category
, idx
):
1442 """Select a top-level "header" item (ex: the Staged parent item)
1444 Return True when a top-level item is selected.
1446 is_top_level_item
= category
== HEADER_IDX
1447 if is_top_level_item
:
1448 root_item
= widget
.invisibleRootItem()
1449 item
= root_item
.child(idx
)
1451 if item
is not None and item
.childCount() == 0:
1452 # The item now has no children. Select a different top-level item
1453 # corresponding to the previously selected item.
1454 if idx
== STAGED_IDX
:
1455 # If "Staged" was previously selected try "Modified" and "Untracked".
1456 item
= _get_first_item_with_children(
1457 root_item
.child(MODIFIED_IDX
), root_item
.child(UNTRACKED_IDX
)
1459 elif idx
== UNMERGED_IDX
:
1460 # If "Unmerged" was previously selected try "Staged".
1461 item
= _get_first_item_with_children(root_item
.child(STAGED_IDX
))
1462 elif idx
== MODIFIED_IDX
:
1463 # If "Modified" was previously selected try "Staged" or "Untracked".
1464 item
= _get_first_item_with_children(
1465 root_item
.child(STAGED_IDX
), root_item
.child(UNTRACKED_IDX
)
1467 elif idx
== UNTRACKED_IDX
:
1468 # If "Untracked" was previously selected try "Staged".
1469 item
= _get_first_item_with_children(root_item
.child(STAGED_IDX
))
1471 if item
is not None:
1472 with qtutils
.BlockSignals(widget
):
1473 widget
.setCurrentItem(item
)
1474 item
.setSelected(True)
1475 widget
.show_selection()
1476 return is_top_level_item
1479 def _get_first_item_with_children(*items
):
1480 """Return the first item that contains child items"""
1482 if item
.childCount() > 0:
1487 def _transplant_selection_across_sections(
1488 category
, idx
, previous_contents
, saved_selection
1490 """Transplant the selection to a different category"""
1491 # This function is used when the selection would otherwise become empty.
1492 # Apply heuristics to select the items based on the previous state.
1493 if not previous_contents
:
1495 staged
, unmerged
, modified
, untracked
= saved_selection
1496 prev_staged
, prev_unmerged
, prev_modified
, prev_untracked
= previous_contents
1498 # The current set of paths.
1499 staged_paths
= staged
[NEW_PATHS_IDX
]
1500 unmerged_paths
= unmerged
[NEW_PATHS_IDX
]
1501 modified_paths
= modified
[NEW_PATHS_IDX
]
1502 untracked_paths
= untracked
[NEW_PATHS_IDX
]
1504 # These callbacks select a path in the corresponding widget subtree lists.
1505 select_staged
= staged
[SELECT_FN_IDX
]
1506 select_unmerged
= unmerged
[SELECT_FN_IDX
]
1507 select_modified
= modified
[SELECT_FN_IDX
]
1508 select_untracked
= untracked
[SELECT_FN_IDX
]
1510 if category
== STAGED_IDX
:
1511 # Staged files can become Unmerged, Modified or Untracked.
1512 # If we previously had a staged file selected then try to select
1513 # it in either the Unmerged, Modified or Untracked sections.
1515 old_path
= prev_staged
[idx
]
1518 if old_path
in unmerged_paths
:
1519 select_unmerged(old_path
, current
=True)
1520 elif old_path
in modified_paths
:
1521 select_modified(old_path
, current
=True)
1522 elif old_path
in untracked_paths
:
1523 select_untracked(old_path
, current
=True)
1525 elif category
== UNMERGED_IDX
:
1526 # Unmerged files can become Staged, Modified or Untracked.
1527 # If we previously had an unmerged file selected then try to select it in
1528 # the Staged, Modified or Untracked sections.
1530 old_path
= prev_unmerged
[idx
]
1533 if old_path
in staged_paths
:
1534 select_staged(old_path
, current
=True)
1535 elif old_path
in modified_paths
:
1536 select_modified(old_path
, current
=True)
1537 elif old_path
in untracked_paths
:
1538 select_untracked(old_path
, current
=True)
1540 elif category
== MODIFIED_IDX
:
1541 # If we previously had a modified file selected then try to select
1542 # it in either the Staged or Untracked sections.
1544 old_path
= prev_modified
[idx
]
1547 if old_path
in staged_paths
:
1548 select_staged(old_path
, current
=True)
1549 elif old_path
in untracked_paths
:
1550 select_untracked(old_path
, current
=True)
1552 elif category
== UNTRACKED_IDX
:
1553 # If we previously had an untracked file selected then try to select
1554 # it in the Modified or Staged section. Modified is less common, but
1555 # it's possible for a file to be untracked and then the user adds and
1556 # modifies the file before we've refreshed our state.
1558 old_path
= prev_untracked
[idx
]
1561 if old_path
in modified_paths
:
1562 select_modified(old_path
, current
=True)
1563 elif old_path
in staged_paths
:
1564 select_staged(old_path
, current
=True)